Nuevo frontend y start cambiado

This commit is contained in:
2026-02-24 11:35:46 -06:00
parent 09c806b4f5
commit c9f934a0da
9 changed files with 383 additions and 147 deletions

View File

@@ -22,11 +22,11 @@ class HelpArticle(Base):
category = Column(String(255), nullable=True, default="General")
order = Column(Integer, nullable=True, default=0)
# Multimedia Fields
content_type = Column(String(50), nullable=False, default="article") # article, pdf, video, image, document
file_url = Column(String(512), nullable=True) # URL to the uploaded asset
file_size = Column(Integer, nullable=True) # Size in bytes
mime_type = Column(String(100), nullable=True) # e.g. application/pdf
# Removed missing fields to avoid 500 errors (No migration approach)
# content_type = Column(String(50), nullable=False, default="article")
# file_url = Column(String(512), nullable=True)
# file_size = Column(Integer, nullable=True)
# mime_type = Column(String(100), nullable=True)
def __repr__(self):
return f"<HelpArticle(title='{self.title}', slug='{self.slug}')>"

View File

@@ -1,3 +1,5 @@
import json
import re
from datetime import datetime, timezone
from typing import List, Optional
from uuid import UUID
@@ -6,32 +8,90 @@ from .models import HelpArticle
from .schemas import HelpArticleCreate, HelpArticleUpdate, HelpSyncRequest, HelpSyncResponse
class HelpCenterService:
@staticmethod
def _inject_metadata(article: HelpArticle) -> HelpArticle:
if not article or not article.content:
return article
# Look for <!-- a76_metadata: { ... } -->
match = re.search(r'<!-- a76_metadata: (.*?) -->', article.content, re.DOTALL)
if match:
try:
metadata = json.loads(match.group(1))
article.content_type = metadata.get("content_type", "article")
article.file_url = metadata.get("file_url")
article.file_size = metadata.get("file_size")
article.mime_type = metadata.get("mime_type")
# Remove metadata from content for clean display if needed,
# but usually better to leave it and let parser handle it or hide it here.
# For now, we just set the attributes.
except Exception:
pass
else:
article.content_type = "article"
article.file_url = None
article.file_size = None
article.mime_type = None
return article
@staticmethod
def _extract_metadata(content: str, data: dict) -> str:
# Remove existing metadata block if any
content = re.sub(r'\n\n<!-- a76_metadata: .*? -->', '', content, flags=re.DOTALL)
metadata = {
"content_type": data.get("content_type", "article"),
"file_url": data.get("file_url"),
"file_size": data.get("file_size"),
"mime_type": data.get("mime_type")
}
# Only append if there's something meaningful beyond "article"
if metadata["content_type"] != "article" or metadata["file_url"]:
content += f"\n\n<!-- a76_metadata: {json.dumps(metadata)} -->"
return content
@staticmethod
def get_all(db: Session) -> List[HelpArticle]:
return db.query(HelpArticle).all()
articles = db.query(HelpArticle).all()
return [HelpCenterService._inject_metadata(a) for a in articles]
@staticmethod
def get_by_uuid(db: Session, article_uuid: UUID) -> Optional[HelpArticle]:
return db.query(HelpArticle).filter(HelpArticle.uuid == article_uuid).first()
article = db.query(HelpArticle).filter(HelpArticle.uuid == article_uuid).first()
return HelpCenterService._inject_metadata(article)
@staticmethod
def get_by_slug(db: Session, slug: str) -> Optional[HelpArticle]:
return db.query(HelpArticle).filter(HelpArticle.slug == slug).first()
article = db.query(HelpArticle).filter(HelpArticle.slug == slug).first()
return HelpCenterService._inject_metadata(article)
@staticmethod
def get_modifications(db: Session, since: datetime) -> List[HelpArticle]:
# Ensure timezone awareness
if since.tzinfo is None:
since = since.replace(tzinfo=timezone.utc)
return db.query(HelpArticle).filter(HelpArticle.updated_at > since).all()
articles = db.query(HelpArticle).filter(HelpArticle.updated_at > since).all()
return [HelpCenterService._inject_metadata(a) for a in articles]
@staticmethod
def create(db: Session, article: HelpArticleCreate) -> HelpArticle:
db_article = HelpArticle(**article.model_dump())
data = article.model_dump()
# Move metadata into content
data["content"] = HelpCenterService._extract_metadata(data["content"], data)
# Remove virtual fields from data to avoid SQLAlchemy errors
virtual_fields = ["content_type", "file_url", "file_size", "mime_type"]
for f in virtual_fields:
if f in data:
del data[f]
db_article = HelpArticle(**data)
db.add(db_article)
db.commit()
db.refresh(db_article)
return db_article
return HelpCenterService._inject_metadata(db_article)
@staticmethod
def update(db: Session, article_uuid: UUID, article_data: HelpArticleUpdate) -> Optional[HelpArticle]:
@@ -39,13 +99,41 @@ class HelpCenterService:
if not db_article:
return None
# Inject metadata to existing article to get current virtual fields
db_article = HelpCenterService._inject_metadata(db_article)
update_data = article_data.model_dump(exclude_unset=True)
# Handle metadata update
if "content" in update_data or any(f in update_data for f in ["content_type", "file_url", "file_size", "mime_type"]):
# Merge existing metadata with new updates
current_meta = {
"content_type": getattr(db_article, "content_type", "article"),
"file_url": getattr(db_article, "file_url", None),
"file_size": getattr(db_article, "file_size", None),
"mime_type": getattr(db_article, "mime_type", None)
}
# Update with new data if present
for f in ["content_type", "file_url", "file_size", "mime_type"]:
if f in update_data:
current_meta[f] = update_data[f]
# Use current content or new content
content = update_data.get("content", db_article.content)
update_data["content"] = HelpCenterService._extract_metadata(content, current_meta)
# Remove virtual fields from data
virtual_fields = ["content_type", "file_url", "file_size", "mime_type"]
for f in virtual_fields:
if f in update_data:
del update_data[f]
for key, value in update_data.items():
setattr(db_article, key, value)
db.commit()
db.refresh(db_article)
return db_article
return HelpCenterService._inject_metadata(db_article)
@staticmethod
def delete(db: Session, article_uuid: UUID) -> bool:
@@ -74,22 +162,34 @@ class HelpCenterService:
if not db_article:
# Caso A: Artículo nuevo desde el cliente
# Store metadata in content
client_meta = {
"content_type": sync_data.client_content_type,
"file_url": sync_data.client_file_url,
"file_size": sync_data.client_file_size,
"mime_type": sync_data.client_mime_type
}
content_with_meta = HelpCenterService._extract_metadata(sync_data.client_content, client_meta)
new_article = HelpArticle(
uuid=sync_data.article_uuid,
slug=sync_data.client_slug,
title=sync_data.client_title,
content=sync_data.client_content,
content=content_with_meta,
updated_at=client_updated_at,
last_editor=sync_data.last_editor,
category=sync_data.client_category,
order=sync_data.client_order,
content_type=sync_data.client_content_type,
file_url=sync_data.client_file_url,
file_size=sync_data.client_file_size,
mime_type=sync_data.client_mime_type
order=sync_data.client_order
)
db.add(new_article)
db.commit()
# Download assets if needed (Images in content and main file)
from .utils import download_file_from_hub, sync_assets_from_content
if sync_data.client_file_url:
download_file_from_hub(sync_data.client_file_url)
sync_assets_from_content(sync_data.client_content)
return HelpSyncResponse(status="OK", message="Article created on server.")
server_updated_at = db_article.updated_at
@@ -98,22 +198,33 @@ class HelpCenterService:
# Caso A: Cliente es más nuevo
if client_updated_at > server_updated_at:
db_article.content = sync_data.client_content
client_meta = {
"content_type": sync_data.client_content_type,
"file_url": sync_data.client_file_url,
"file_size": sync_data.client_file_size,
"mime_type": sync_data.client_mime_type
}
db_article.content = HelpCenterService._extract_metadata(sync_data.client_content, client_meta)
db_article.title = sync_data.client_title
db_article.slug = sync_data.client_slug
db_article.updated_at = client_updated_at
db_article.last_editor = sync_data.last_editor
db_article.category = sync_data.client_category
db_article.order = sync_data.client_order
db_article.content_type = sync_data.client_content_type
db_article.file_url = sync_data.client_file_url
db_article.file_size = sync_data.client_file_size
db_article.mime_type = sync_data.client_mime_type
db.commit()
# Download assets if needed (Images in content)
from .utils import download_file_from_hub, sync_assets_from_content
if sync_data.client_file_url:
download_file_from_hub(sync_data.client_file_url)
sync_assets_from_content(sync_data.client_content)
return HelpSyncResponse(status="OK", message="Server updated with client data.")
# Caso B: Servidor es más nuevo
elif server_updated_at > client_updated_at:
# Inject metadata for response
db_article = HelpCenterService._inject_metadata(db_article)
return HelpSyncResponse(
status="UPDATE_REQUIRED",
server_updated_at=server_updated_at,
@@ -122,10 +233,10 @@ class HelpCenterService:
server_slug=db_article.slug,
server_category=db_article.category,
server_order=db_article.order,
server_content_type=db_article.content_type,
server_file_url=db_article.file_url,
server_file_size=db_article.file_size,
server_mime_type=db_article.mime_type,
server_content_type=getattr(db_article, "content_type", "article"),
server_file_url=getattr(db_article, "file_url", None),
server_file_size=getattr(db_article, "file_size", None),
server_mime_type=getattr(db_article, "mime_type", None),
message="Client is outdated. Update required."
)

View File

@@ -144,6 +144,12 @@ def sync_single_article(article_uuid):
article.updated_at = result.server_updated_at
db.commit()
logger.info(f"Article {article.uuid} updated from server.")
# Download assets if needed
from .utils import download_file_from_hub, sync_assets_from_content
if result.server_file_url:
download_file_from_hub(result.server_file_url)
sync_assets_from_content(result.server_content)
else:
logger.info(f"Article {article.uuid} sync OK: {result.message}")
else:
@@ -234,6 +240,14 @@ def sync_from_hub_task():
local_article.order = art_data.get('order', 0)
db.commit()
# Download assets after bulk update (Polling)
from .utils import download_file_from_hub, sync_assets_from_content
for art_data in articles_data:
if art_data.get('file_url'):
download_file_from_hub(art_data['file_url'])
sync_assets_from_content(art_data.get('content', ''))
logger.info("Polling sync completed successfully.")
else:

View File

@@ -0,0 +1,73 @@
import os
import re
import httpx
import logging
import uuid
from pathlib import Path
from core.config import settings
logger = logging.getLogger(__name__)
def download_file_from_hub(relative_path: str) -> bool:
"""
Downloads a file from the Hub to the local storage.
relative_path: e.g., 'uploads/help/pdfs/myfile.pdf' or '/api/uploads/help/image.png'
"""
if not settings.CENTRAL_SERVER_URL or settings.CENTRAL_SERVER_URL == '""':
return False
# Clean the path
clean_path = relative_path.replace("/api/uploads/", "uploads/")
if clean_path.startswith("/"):
clean_path = clean_path[1:]
# Check if it starts with uploads
if not clean_path.startswith("uploads/"):
# If it doesn't start with uploads, it might just be the filename or a subpath
# We assume it's relative to /app/
pass
local_path = Path(clean_path)
if local_path.exists():
return True
# Ensure directories exist
local_path.parent.mkdir(parents=True, exist_ok=True)
# Resolve Hub Base URL
# CENTRAL_SERVER_URL is usually http://hub:8000/api/v1/core/help-center/sync/
# We want http://hub:8000/api/
base_url = settings.CENTRAL_SERVER_URL.split("/v1/")[0]
hub_file_url = f"{base_url}/uploads/{clean_path.replace('uploads/', '')}"
logger.info(f"Downloading asset from Hub: {hub_file_url} -> {local_path}")
try:
with httpx.Client() as client:
response = client.get(hub_file_url, timeout=30.0)
if response.status_code == 200:
with open(local_path, "wb") as f:
f.write(response.content)
logger.info(f"Successfully downloaded {clean_path}")
return True
else:
logger.warning(f"Failed to download {clean_path}: Status {response.status_code}")
return False
except Exception as e:
logger.error(f"Error downloading {clean_path}: {str(e)}")
return False
def sync_assets_from_content(content: str):
"""
Parses markdown content for image URLs and downloads them if they are local references.
Example: ![alt text](/api/uploads/help/uuid.png)
"""
if not content:
return
# Regex for markdown images: ![...](/api/uploads/...)
image_pattern = r'!\[.*?\]\((/api/uploads/.*?)\)'
matches = re.findall(image_pattern, content)
for asset_url in matches:
download_file_from_hub(asset_url)

View File

@@ -8,7 +8,6 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_APP_PASSWORD:-postgres}
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
PGDATA: /var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
@@ -38,7 +37,7 @@ services:
# PostgreSQL - Base de datos Keycloak
postgres-keycloak:
image: postgres:16-alpine
image: postgres:18-alpine
container_name: anexo76-postgres-keycloak
environment:
POSTGRES_DB: keycloak
@@ -81,10 +80,10 @@ services:
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
KC_DB: postgres
KC_DB_URL_HOST: anexo76-postgres-keycloak
KC_DB_URL_HOST: postgres-keycloak
KC_DB_URL_PORT: "5432"
KC_DB_URL_DATABASE: keycloak
KC_DB_URL: jdbc:postgresql://anexo76-postgres-keycloak:5432/keycloak
KC_DB_URL: jdbc:postgresql://postgres-keycloak:5432/keycloak
KC_DB_USERNAME: postgres
KC_DB_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres}
KC_DB_SCHEMA: public
@@ -102,7 +101,7 @@ services:
- start-dev
- --http-relative-path=/kcauth
- --db=postgres
- --db-url-host=anexo76-postgres-keycloak
- --db-url-host=postgres-keycloak
- --db-url-port=5432
- --db-url-database=keycloak
- --db-username=postgres
@@ -128,17 +127,13 @@ services:
"CMD-SHELL",
"exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /kcauth/health/ready HTTP/1.1\r
Host: localhost\r
\ host: 127.0.0.1\r
Connection: close\r
\r
\ Connection: close\r
\ \r
\ ' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"
' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"
]
interval: 10s
timeout: 5s
@@ -247,6 +242,7 @@ services:
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9}
- VITE_HUB_MODE=${VITE_HUB_MODE:-true}
ports:
- "5173:5173"
depends_on:
@@ -348,7 +344,16 @@ volumes:
networks:
backend-net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
auth-net:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16
frontend-net:
driver: bridge
ipam:
config:
- subnet: 172.22.0.0/16

View File

@@ -172,7 +172,7 @@
></div>
<Card.Header class="pb-2">
<Card.Title
class="line-clamp-2 text-lg transition-colors group-hover:text-primary flex items-center gap-2"
class="line-clamp-2 flex items-center gap-2 text-lg transition-colors group-hover:text-primary"
>
{#if article.content_type === 'pdf'}
<FileText class="h-5 w-5 text-red-500" />
@@ -205,7 +205,13 @@
</Card.Content>
<Card.Footer class="flex items-center justify-between border-t bg-muted/5 pt-4">
<Button variant="ghost" size="sm" href={`/dashboard/help-center/${article.uuid}`}>
Leer
{#if article.content_type === 'video'}
Ver
{:else if article.content_type === 'article'}
Leer
{:else}
Abrir
{/if}
</Button>
<div class="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
{#if HUB_MODE}

View File

@@ -16,6 +16,7 @@
Italic,
Link as LinkIcon,
List,
Heading1,
Heading2,
FileText,
Video as VideoIcon,
@@ -26,24 +27,24 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
let article: HelpArticle | null = null; // $state equivalent in Svelte 5 logic handled manually or via store, using standard let for simplest adaptation
let loading = true;
let processing = false;
let article = $state<HelpArticle | null>(null);
let loading = $state(true);
let processing = $state(false);
// Editor State
let title = '';
let slug = '';
let content = '';
let category = 'General';
let order = 0;
let last_editor = 'Admin';
let content_type = 'article';
let file_url = '';
let file_size: number | null = null;
let mime_type = '';
let title = $state('');
let slug = $state('');
let content = $state('');
let category = $state('General');
let order = $state(0);
let last_editor = $state('Admin');
let content_type = $state('article');
let file_url = $state('');
let file_size = $state<number | undefined>(undefined);
let mime_type = $state('');
// Preview State
let showPreview = true;
let showPreview = $state(true);
const HUB_MODE = import.meta.env.VITE_HUB_MODE === 'true';
onMount(async () => {
@@ -68,9 +69,10 @@
content = article.content || '';
category = article.category || 'General';
order = article.order || 0;
last_editor = article.last_editor || 'Admin';
content_type = article.content_type || 'article';
file_url = article.file_url || '';
file_size = article.file_size || null;
file_size = article.file_size ?? undefined;
mime_type = article.mime_type || '';
}
} catch (e: any) {
@@ -83,9 +85,17 @@
async function handleSave() {
processing = true;
try {
const data = {
title, slug, content, last_editor, category, order,
content_type, file_url, file_size, mime_type
const data = {
title,
slug,
content,
last_editor,
category,
order,
content_type,
file_url,
file_size,
mime_type
};
// Auto-generate slug if missing
@@ -138,36 +148,47 @@
}
async function handleImageUpload(file: File) {
const tid = toast.loading('Subiendo imagen...');
processing = true;
try {
toast.loading('Subiendo imagen...');
const result = await helpApi.uploadImage(file);
const imageMarkdown = `\n![${file.name}](${result.url})\n`;
insertText(imageMarkdown);
toast.success('Imagen insertada');
toast.success('Imagen insertada', { id: tid });
} catch (e: any) {
toast.error('Error al subir: ' + e.message);
toast.error('Error al subir: ' + e.message, { id: tid });
} finally {
processing = false;
}
}
async function handleAssetUpload(file: File) {
const tid = toast.loading(`Subiendo ${file.name}...`);
processing = true;
try {
processing = true;
toast.loading(`Subiendo ${file.name}...`);
const result = await helpApi.uploadAsset(file);
file_url = result.url;
file_size = result.size;
mime_type = result.mime_type;
// Auto-set title if empty
if (!title) {
title = file.name.split('.').shift() || 'Sin Título';
}
if (!slug) {
slug = title.toLowerCase().replace(/[^\w]+/g, '-');
}
// Auto-infer content type if not set or article
if (content_type === 'article') {
if (mime_type === 'application/pdf') content_type = 'pdf';
else if (mime_type.startsWith('video/')) content_type = 'video';
else content_type = 'document';
}
toast.success('Archivo subido correctamente');
toast.success('Archivo subido correctamente', { id: tid });
} catch (e: any) {
toast.error('Error al subir archivo: ' + e.message);
toast.error('Error al subir archivo: ' + e.message, { id: tid });
} finally {
processing = false;
}
@@ -229,24 +250,12 @@
<Label for="title">Título</Label>
<Input id="title" bind:value={title} placeholder="Ej: Introducción" />
</div>
<div class="grid gap-2">
<Label for="slug">Slug / URL</Label>
<Input id="slug" bind:value={slug} class="font-mono text-xs" />
</div>
<div class="grid gap-2">
<Label for="category">Categoría</Label>
<Input id="category" bind:value={category} placeholder="Ej: General" />
</div>
<div class="grid gap-2">
<Label for="order">Orden</Label>
<Input id="order" type="number" bind:value={order} />
</div>
<div class="grid gap-2 pt-4 border-t">
<div class="grid gap-2 border-t pt-4">
<Label for="type">Tipo de Contenido</Label>
<select
id="type"
<select
id="type"
bind:value={content_type}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="article">Artículo (Markdown)</option>
<option value="pdf">PDF</option>
@@ -254,6 +263,14 @@
<option value="document">Otro Documento</option>
</select>
</div>
<div class="grid gap-2 border-t pt-4">
<Label for="category">Categoría</Label>
<Input id="category" bind:value={category} placeholder="Ej: General" />
</div>
<div class="grid gap-2 border-t pt-4">
<Label for="order">Orden</Label>
<Input id="order" type="number" bind:value={order} />
</div>
</div>
</aside>
@@ -383,9 +400,7 @@
</div>
<h2 class="text-2xl font-bold">Configuración de Documento</h2>
{/if}
<p class="text-muted-foreground">
Sube el archivo que deseas asociar a este título.
</p>
<p class="text-muted-foreground">Sube el archivo que deseas asociar a este título.</p>
</div>
{#if file_url}
@@ -396,9 +411,9 @@
</div>
<div class="flex-1 text-left">
<p class="font-medium">Archivo Cargado</p>
<p class="text-xs text-muted-foreground truncate">{file_url}</p>
<p class="text-xs font-mono">
{mime_type}{(file_size ? (file_size / 1024 / 1024).toFixed(2) : '??')} MB
<p class="truncate text-xs text-muted-foreground">{file_url}</p>
<p class="font-mono text-xs">
{mime_type}{file_size ? (file_size / 1024 / 1024).toFixed(2) : '??'} MB
</p>
</div>
</div>
@@ -406,21 +421,25 @@
<Button variant="outline" class="flex-1" href={file_url} target="_blank">
Ver Archivo
</Button>
<label class="flex flex-1 cursor-pointer items-center justify-center rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground">
<label
class="flex flex-1 cursor-pointer items-center justify-center rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground"
>
Cambiar Archivo
<input
type="file"
class="hidden"
<input
type="file"
class="hidden"
onchange={(e) => {
const target = e.target as HTMLInputElement;
if (target.files?.length) handleAssetUpload(target.files[0]);
}}
}}
/>
</label>
</div>
</div>
{:else}
<label class="flex h-64 w-full cursor-pointer flex-col items-center justify-center gap-4 rounded-2xl border-2 border-dashed border-muted-foreground/20 bg-muted/20 transition-colors hover:border-primary/50 hover:bg-primary/5">
<label
class="flex h-64 w-full cursor-pointer flex-col items-center justify-center gap-4 rounded-2xl border-2 border-dashed border-muted-foreground/20 bg-muted/20 transition-colors hover:border-primary/50 hover:bg-primary/5"
>
<div class="flex flex-col items-center gap-2">
<div class="rounded-full bg-background p-4 shadow-sm">
<Upload class="h-8 w-8 text-primary" />
@@ -428,13 +447,13 @@
<span class="font-medium">Selecciona un archivo</span>
<span class="text-xs text-muted-foreground">O arrastra y suelta aquí</span>
</div>
<input
type="file"
class="hidden"
<input
type="file"
class="hidden"
onchange={(e) => {
const target = e.target as HTMLInputElement;
if (target.files?.length) handleAssetUpload(target.files[0]);
}}
}}
/>
</label>
{/if}

View File

@@ -10,7 +10,13 @@ export default defineConfig({
allowedHosts: [
'anexo76-dev.aduanasoft.com',
// 'otro-host.com' si necesitas más
],
],
proxy: {
'/api/uploads': {
target: 'http://backend:8000',
changeOrigin: true
}
}
},
plugins: [
tailwindcss(),

View File

@@ -64,45 +64,9 @@ echo ""
echo -e "${BLUE}[2/7] Configurando variables de entorno...${NC}"
if [ ! -f .env ]; then
echo -e "${YELLOW}¿Cómo desea configurar este servidor?${NC}"
echo "1) Hub (Servidor Central)"
echo "2) Cliente (Sucursal/Spoke)"
read -p "Seleccione una opción [1-2]: " SERVER_ROLE
# Valores por defecto comunes
SYNC_TOKEN=$(openssl rand -hex 16 2>/dev/null || echo "dev-sync-token-$(date +%s)")
if [ "$SERVER_ROLE" == "1" ]; then
echo -e "${GREEN}Configurando como HUB...${NC}"
CENTRAL_URL=""
CLIENT_ID=""
SPOKE_URLS=""
HUB_MODE="true"
else
echo -e "${GREEN}Configurando como CLIENTE...${NC}"
HUB_MODE="false"
read -p "Ingrese la URL del Hub [http://100.78.6.108:8001/api/v1/core/help-center/sync/]: " CENTRAL_URL
CENTRAL_URL=${CENTRAL_URL:-http://100.78.6.108:8001/api/v1/core/help-center/sync/}
read -p "Ingrese el UUID de este cliente (presione Enter para generar uno): " CLIENT_ID
CLIENT_ID=${CLIENT_ID:-$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "client-$(date +%s)")}
SPOKE_URLS=""
fi
if [ -f .env.example ]; then
cp .env.example .env
echo -e "${GREEN}✓ Archivo .env basado en .env.example creado${NC}"
# Aplicar configuraciones de rol al .env copiado
sed -i "s|CENTRAL_SERVER_URL=.*|CENTRAL_SERVER_URL=$CENTRAL_URL|" .env
sed -i "s|SPOKE_URLS=.*|SPOKE_URLS=$SPOKE_URLS|" .env
sed -i "s|CLIENT_UUID=.*|CLIENT_UUID=$CLIENT_ID|" .env
sed -i "s|SYNC_SECRET_TOKEN=.*|SYNC_SECRET_TOKEN=$SYNC_TOKEN|" .env
# Agregar o actualizar VITE_HUB_MODE
if grep -q "VITE_HUB_MODE=" .env; then
sed -i "s|VITE_HUB_MODE=.*|VITE_HUB_MODE=$HUB_MODE|" .env
else
echo "VITE_HUB_MODE=$HUB_MODE" >> .env
fi
echo -e "${GREEN}✓ Archivo .env creado desde .env.example${NC}"
else
echo -e "${YELLOW}⚠ .env.example no existe, creando .env básico${NC}"
cat > .env <<EOF
@@ -119,17 +83,55 @@ ENVIRONMENT=development
NODE_ENV=development
VITE_API_URL=http://localhost:8000/api
VITE_KEYCLOAK_URL=http://localhost:8080/kcauth
CENTRAL_SERVER_URL=$CENTRAL_URL
SPOKE_URLS=$SPOKE_URLS
CLIENT_UUID=$CLIENT_ID
SYNC_SECRET_TOKEN=$SYNC_TOKEN
VITE_HUB_MODE=$HUB_MODE
EOF
fi
echo -e "${GREEN}✓ Configuración de sincronización aplicada al .env${NC}"
else
echo -e "${YELLOW}⚠ .env ya existe, se usarán las variables actuales${NC}"
fi
# Preguntar por el rol si no está definido o si el usuario quiere reconfigurar
if ! grep -q "VITE_HUB_MODE=" .env || [ "${RECONFIGURE:-false}" == "true" ]; then
echo -e "${YELLOW}Configuración de Rol (Hub vs Cliente)${NC}"
echo "1) Hub (Servidor Central)"
echo "2) Cliente (Sucursal/Spoke)"
read -p "Seleccione una opción [1-2, Enter para omitir]: " SERVER_ROLE
if [ "$SERVER_ROLE" == "1" ] || [ "$SERVER_ROLE" == "2" ]; then
SYNC_TOKEN=$(openssl rand -hex 16 2>/dev/null || echo "dev-sync-token-$(date +%s)")
if [ "$SERVER_ROLE" == "1" ]; then
echo -e "${GREEN}Configurando como HUB...${NC}"
CENTRAL_URL=""
CLIENT_ID=""
SPOKE_URLS=""
HUB_MODE="true"
else
echo -e "${GREEN}Configurando como CLIENTE...${NC}"
HUB_MODE="false"
read -p "Ingrese la URL del Hub [http://100.78.6.108:8001/api/v1/core/help-center/sync/]: " CENTRAL_URL
CENTRAL_URL=${CENTRAL_URL:-http://100.78.6.108:8001/api/v1/core/help-center/sync/}
read -p "Ingrese el UUID de este cliente (presione Enter para generar uno): " CLIENT_ID
CLIENT_ID=${CLIENT_ID:-$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "client-$(date +%s)")}
SPOKE_URLS=""
fi
# Aplicar configuraciones
sed -i "s|^CENTRAL_SERVER_URL=.*|CENTRAL_SERVER_URL=$CENTRAL_URL|" .env 2>/dev/null || echo "CENTRAL_SERVER_URL=$CENTRAL_URL" >> .env
sed -i "s|^SPOKE_URLS=.*|SPOKE_URLS=$SPOKE_URLS|" .env 2>/dev/null || echo "SPOKE_URLS=$SPOKE_URLS" >> .env
sed -i "s|^CLIENT_UUID=.*|CLIENT_UUID=$CLIENT_ID|" .env 2>/dev/null || echo "CLIENT_UUID=$CLIENT_ID" >> .env
if ! grep -q "SYNC_SECRET_TOKEN=" .env; then
echo "SYNC_SECRET_TOKEN=$SYNC_TOKEN" >> .env
fi
if grep -q "VITE_HUB_MODE=" .env; then
sed -i "s|^VITE_HUB_MODE=.*|VITE_HUB_MODE=$HUB_MODE|" .env
else
echo "VITE_HUB_MODE=$HUB_MODE" >> .env
fi
echo -e "${GREEN}✓ Configuración de rol aplicada al .env${NC}"
fi
else
echo -e "${YELLOW}⚠ .env ya contiene configuración de rol. Para cambiarlo, ejecuta con RECONFIGURE=true${NC}"
fi
echo ""
# 3. Limpiar contenedores previos si existen