Nuevo forntend con datos multimedia
This commit is contained in:
@@ -21,6 +21,12 @@ class HelpArticle(Base):
|
|||||||
# Library Mode Fields
|
# Library Mode Fields
|
||||||
category = Column(String(255), nullable=True, default="General")
|
category = Column(String(255), nullable=True, default="General")
|
||||||
order = Column(Integer, nullable=True, default=0)
|
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
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<HelpArticle(title='{self.title}', slug='{self.slug}')>"
|
return f"<HelpArticle(title='{self.title}', slug='{self.slug}')>"
|
||||||
|
|||||||
@@ -97,6 +97,40 @@ def upload_help_image(file: UploadFile = File(...)):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/upload-asset/")
|
||||||
|
def upload_help_asset(file: UploadFile = File(...)):
|
||||||
|
"""Sube cualquier tipo de archivo (PDF, Video, etc.) para la biblioteca."""
|
||||||
|
try:
|
||||||
|
file_ext = os.path.splitext(file.filename)[1].lower()
|
||||||
|
new_filename = f"{uuid.uuid4()}{file_ext}"
|
||||||
|
|
||||||
|
# Guardar en una carpeta segun el tipo o general
|
||||||
|
folder = "uploads/help/assets"
|
||||||
|
if file_ext in ['.pdf']:
|
||||||
|
folder = "uploads/help/pdfs"
|
||||||
|
elif file_ext in ['.mp4', '.mov', '.avi']:
|
||||||
|
folder = "uploads/help/videos"
|
||||||
|
|
||||||
|
file_location = f"{folder}/{new_filename}"
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
|
||||||
|
with open(file_location, "wb+") as buffer:
|
||||||
|
shutil.copyfileobj(file.file, buffer)
|
||||||
|
|
||||||
|
# Get file size
|
||||||
|
file_size = os.path.getsize(file_location)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"url": f"/api/{file_location}",
|
||||||
|
"filename": file.filename,
|
||||||
|
"size": file_size,
|
||||||
|
"mime_type": file.content_type
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@router.get("/articles/", response_model=List[HelpArticleInDB])
|
@router.get("/articles/", response_model=List[HelpArticleInDB])
|
||||||
def list_articles(db: Session = Depends(get_core_db)):
|
def list_articles(db: Session = Depends(get_core_db)):
|
||||||
"""Lista todos los artículos de ayuda."""
|
"""Lista todos los artículos de ayuda."""
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ class HelpArticleBase(BaseModel):
|
|||||||
last_editor: str
|
last_editor: str
|
||||||
category: Optional[str] = "General"
|
category: Optional[str] = "General"
|
||||||
order: Optional[int] = 0
|
order: Optional[int] = 0
|
||||||
|
content_type: str = "article"
|
||||||
|
file_url: Optional[str] = None
|
||||||
|
file_size: Optional[int] = None
|
||||||
|
mime_type: Optional[str] = None
|
||||||
|
|
||||||
class HelpArticleCreate(HelpArticleBase):
|
class HelpArticleCreate(HelpArticleBase):
|
||||||
pass
|
pass
|
||||||
@@ -21,6 +25,10 @@ class HelpArticleUpdate(BaseModel):
|
|||||||
last_editor: Optional[str] = None
|
last_editor: Optional[str] = None
|
||||||
category: Optional[str] = None
|
category: Optional[str] = None
|
||||||
order: Optional[int] = None
|
order: Optional[int] = None
|
||||||
|
content_type: Optional[str] = None
|
||||||
|
file_url: Optional[str] = None
|
||||||
|
file_size: Optional[int] = None
|
||||||
|
mime_type: Optional[str] = None
|
||||||
|
|
||||||
class HelpArticleInDB(HelpArticleBase):
|
class HelpArticleInDB(HelpArticleBase):
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
@@ -38,6 +46,10 @@ class HelpSyncRequest(BaseModel):
|
|||||||
last_editor: str
|
last_editor: str
|
||||||
client_category: Optional[str] = "General"
|
client_category: Optional[str] = "General"
|
||||||
client_order: Optional[int] = 0
|
client_order: Optional[int] = 0
|
||||||
|
client_content_type: str = "article"
|
||||||
|
client_file_url: Optional[str] = None
|
||||||
|
client_file_size: Optional[int] = None
|
||||||
|
client_mime_type: Optional[str] = None
|
||||||
origin_client_uuid: Optional[UUID] = None
|
origin_client_uuid: Optional[UUID] = None
|
||||||
|
|
||||||
class HelpSyncResponse(BaseModel):
|
class HelpSyncResponse(BaseModel):
|
||||||
@@ -48,4 +60,8 @@ class HelpSyncResponse(BaseModel):
|
|||||||
server_slug: Optional[str] = None
|
server_slug: Optional[str] = None
|
||||||
server_category: Optional[str] = None
|
server_category: Optional[str] = None
|
||||||
server_order: Optional[int] = None
|
server_order: Optional[int] = None
|
||||||
|
server_content_type: Optional[str] = None
|
||||||
|
server_file_url: Optional[str] = None
|
||||||
|
server_file_size: Optional[int] = None
|
||||||
|
server_mime_type: Optional[str] = None
|
||||||
message: str
|
message: str
|
||||||
|
|||||||
@@ -82,7 +82,11 @@ class HelpCenterService:
|
|||||||
updated_at=client_updated_at,
|
updated_at=client_updated_at,
|
||||||
last_editor=sync_data.last_editor,
|
last_editor=sync_data.last_editor,
|
||||||
category=sync_data.client_category,
|
category=sync_data.client_category,
|
||||||
order=sync_data.client_order
|
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
|
||||||
)
|
)
|
||||||
db.add(new_article)
|
db.add(new_article)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -101,6 +105,10 @@ class HelpCenterService:
|
|||||||
db_article.last_editor = sync_data.last_editor
|
db_article.last_editor = sync_data.last_editor
|
||||||
db_article.category = sync_data.client_category
|
db_article.category = sync_data.client_category
|
||||||
db_article.order = sync_data.client_order
|
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()
|
db.commit()
|
||||||
return HelpSyncResponse(status="OK", message="Server updated with client data.")
|
return HelpSyncResponse(status="OK", message="Server updated with client data.")
|
||||||
|
|
||||||
@@ -114,6 +122,10 @@ class HelpCenterService:
|
|||||||
server_slug=db_article.slug,
|
server_slug=db_article.slug,
|
||||||
server_category=db_article.category,
|
server_category=db_article.category,
|
||||||
server_order=db_article.order,
|
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,
|
||||||
message="Client is outdated. Update required."
|
message="Client is outdated. Update required."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -128,13 +128,17 @@ services:
|
|||||||
"CMD-SHELL",
|
"CMD-SHELL",
|
||||||
"exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /kcauth/health/ready HTTP/1.1\r
|
"exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /kcauth/health/ready HTTP/1.1\r
|
||||||
|
|
||||||
host: 127.0.0.1\r
|
|
||||||
|
|
||||||
Connection: close\r
|
\ host: 127.0.0.1\r
|
||||||
|
|
||||||
\r
|
|
||||||
|
|
||||||
' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"
|
\ Connection: close\r
|
||||||
|
|
||||||
|
|
||||||
|
\ \r
|
||||||
|
|
||||||
|
|
||||||
|
\ ' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"
|
||||||
]
|
]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
@@ -297,6 +301,27 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- backend-net
|
- backend-net
|
||||||
|
|
||||||
|
celery_beat:
|
||||||
|
build: ./backend
|
||||||
|
container_name: celery_beat
|
||||||
|
command: celery -A core.celery_app beat --loglevel=info
|
||||||
|
environment:
|
||||||
|
- VALKEY_URL=${VALKEY_URL:-redis://valkey:6379/0}
|
||||||
|
- CENTRAL_SERVER_URL=${CENTRAL_SERVER_URL:-""}
|
||||||
|
- SYNC_SECRET_TOKEN=${SYNC_SECRET_TOKEN:-change-this-sync-token-in-production}
|
||||||
|
- CLIENT_UUID=${CLIENT_UUID:-""}
|
||||||
|
- SPOKE_URLS=${SPOKE_URLS:-""}
|
||||||
|
- CORE_DB_HOST=${CORE_DB_HOST:-postgres-a76}
|
||||||
|
- CORE_DB_PORT=${CORE_DB_PORT:-5432}
|
||||||
|
- CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core}
|
||||||
|
- CORE_DB_USER=${CORE_DB_USER:-postgres}
|
||||||
|
- CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres}
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
- valkey
|
||||||
|
networks:
|
||||||
|
- backend-net
|
||||||
|
|
||||||
valkey:
|
valkey:
|
||||||
image: valkey/valkey:7.2
|
image: valkey/valkey:7.2
|
||||||
container_name: valkey
|
container_name: valkey
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export interface HelpArticle {
|
|||||||
last_editor: string;
|
last_editor: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
|
content_type: string;
|
||||||
|
file_url?: string;
|
||||||
|
file_size?: number;
|
||||||
|
mime_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const helpApi = {
|
export const helpApi = {
|
||||||
@@ -46,7 +50,7 @@ export const helpApi = {
|
|||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
async createArticle(data: { title: string; content: string; slug: string; last_editor: string; category?: string; order?: number }): Promise<HelpArticle> {
|
async createArticle(data: Partial<HelpArticle>): Promise<HelpArticle> {
|
||||||
const response = await fetch(`${BASE_URL}/articles/`, {
|
const response = await fetch(`${BASE_URL}/articles/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getHeaders(),
|
headers: getHeaders(),
|
||||||
@@ -82,5 +86,20 @@ export const helpApi = {
|
|||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Failed to upload image');
|
if (!response.ok) throw new Error('Failed to upload image');
|
||||||
return response.json();
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async uploadAsset(file: File): Promise<{ url: string, filename: string, size: number, mime_type: string }> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(`${BASE_URL}/upload-asset/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...(get(authStore).token ? { 'Authorization': `Bearer ${get(authStore).token}` } : {})
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to upload asset');
|
||||||
|
return response.json();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,24 @@
|
|||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Plus, Trash2, Edit, Loader2, Search, Book, Image as ImageIcon } from 'lucide-svelte';
|
import {
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Edit,
|
||||||
|
Loader2,
|
||||||
|
Search,
|
||||||
|
Book,
|
||||||
|
Image as ImageIcon,
|
||||||
|
FileText,
|
||||||
|
Video,
|
||||||
|
FileCode
|
||||||
|
} from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
let articles: HelpArticle[] = $state([]);
|
let articles: HelpArticle[] = $state([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let searchTerm = $state('');
|
let searchTerm = $state('');
|
||||||
|
const HUB_MODE = import.meta.env.VITE_HUB_MODE === 'true';
|
||||||
|
|
||||||
// Modals state
|
// Modals state
|
||||||
let showCreateModal = $state(false);
|
let showCreateModal = $state(false);
|
||||||
@@ -108,10 +120,12 @@
|
|||||||
<p class="text-muted-foreground">Manuales, Guías y Documentación del Sistema.</p>
|
<p class="text-muted-foreground">Manuales, Guías y Documentación del Sistema.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button href="/dashboard/help-center/editor/new" class="shadow-lg">
|
{#if HUB_MODE}
|
||||||
<Plus class="mr-2 h-4 w-4" />
|
<Button href="/dashboard/help-center/editor/new" class="shadow-lg">
|
||||||
Nuevo Capítulo
|
<Plus class="mr-2 h-4 w-4" />
|
||||||
</Button>
|
Nuevo Capítulo
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -158,8 +172,17 @@
|
|||||||
></div>
|
></div>
|
||||||
<Card.Header class="pb-2">
|
<Card.Header class="pb-2">
|
||||||
<Card.Title
|
<Card.Title
|
||||||
class="line-clamp-2 text-lg transition-colors group-hover:text-primary"
|
class="line-clamp-2 text-lg transition-colors group-hover:text-primary flex items-center gap-2"
|
||||||
>
|
>
|
||||||
|
{#if article.content_type === 'pdf'}
|
||||||
|
<FileText class="h-5 w-5 text-red-500" />
|
||||||
|
{:else if article.content_type === 'video'}
|
||||||
|
<Video class="h-5 w-5 text-blue-500" />
|
||||||
|
{:else if article.content_type === 'article'}
|
||||||
|
<Book class="h-5 w-5 text-primary" />
|
||||||
|
{:else}
|
||||||
|
<FileCode class="h-5 w-5 text-muted-foreground" />
|
||||||
|
{/if}
|
||||||
{article.title}
|
{article.title}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<Card.Description class="text-xs">
|
<Card.Description class="text-xs">
|
||||||
@@ -167,31 +190,42 @@
|
|||||||
</Card.Description>
|
</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="flex-grow pt-2">
|
<Card.Content class="flex-grow pt-2">
|
||||||
<div class="prose prose-sm line-clamp-3 text-sm text-muted-foreground">
|
{#if article.content_type === 'article'}
|
||||||
{@html article.content.replace(/<[^>]*>?/gm, '').substring(0, 150)}...
|
<div class="prose prose-sm line-clamp-3 text-sm text-muted-foreground">
|
||||||
</div>
|
{@html (article.content || '').replace(/<[^>]*>?/gm, '').substring(0, 150)}...
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||||
|
<p>Formato: {article.mime_type || 'Desconocido'}</p>
|
||||||
|
{#if article.file_size}
|
||||||
|
<p>Tamaño: {(article.file_size / 1024 / 1024).toFixed(2)} MB</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
<Card.Footer class="flex items-center justify-between border-t bg-muted/5 pt-4">
|
<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}`}>
|
<Button variant="ghost" size="sm" href={`/dashboard/help-center/${article.uuid}`}>
|
||||||
Leer
|
Leer
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
<div class="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<Button
|
{#if HUB_MODE}
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
class="h-7 w-7"
|
size="icon"
|
||||||
href={`/dashboard/help-center/editor/${article.uuid}`}
|
class="h-7 w-7"
|
||||||
>
|
href={`/dashboard/help-center/editor/${article.uuid}`}
|
||||||
<Edit class="h-3 w-3" />
|
>
|
||||||
</Button>
|
<Edit class="h-3 w-3" />
|
||||||
<Button
|
</Button>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
class="h-7 w-7 text-destructive"
|
size="icon"
|
||||||
onclick={() => openDelete(article)}
|
class="h-7 w-7 text-destructive"
|
||||||
>
|
onclick={() => openDelete(article)}
|
||||||
<Trash2 class="h-3 w-3" />
|
>
|
||||||
</Button>
|
<Trash2 class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@@ -3,7 +3,16 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { helpApi, type HelpArticle } from '$lib/api/help';
|
import { helpApi, type HelpArticle } from '$lib/api/help';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import { Loader2, ArrowLeft, Calendar, User, BookOpen } from 'lucide-svelte';
|
import {
|
||||||
|
Loader2,
|
||||||
|
ArrowLeft,
|
||||||
|
Calendar,
|
||||||
|
User,
|
||||||
|
BookOpen,
|
||||||
|
Download,
|
||||||
|
FileText,
|
||||||
|
ExternalLink
|
||||||
|
} from 'lucide-svelte';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
@@ -124,11 +133,72 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<article
|
{#if article.content_type === 'article'}
|
||||||
class="prose max-w-none prose-slate dark:prose-invert prose-headings:scroll-mt-20 prose-img:rounded-lg prose-img:shadow-md"
|
<article
|
||||||
>
|
class="prose max-w-none prose-slate dark:prose-invert prose-headings:scroll-mt-20 prose-img:rounded-lg prose-img:shadow-md"
|
||||||
{@html renderMarkdown(article.content)}
|
>
|
||||||
</article>
|
{@html renderMarkdown(article.content)}
|
||||||
|
</article>
|
||||||
|
{:else if article.content_type === 'pdf'}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-center justify-between rounded-lg border bg-muted/30 p-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<FileText class="h-8 w-8 text-red-500" />
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">Documento PDF</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{article.file_size ? (article.file_size / 1024 / 1024).toFixed(2) : '??'} MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button variant="outline" size="sm" href={article.file_url} target="_blank">
|
||||||
|
<ExternalLink class="mr-2 h-4 w-4" />
|
||||||
|
Abrir en pestaña
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" href={article.file_url} download={article.title}>
|
||||||
|
<Download class="mr-2 h-4 w-4" />
|
||||||
|
Descargar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- PDF Embed -->
|
||||||
|
<iframe
|
||||||
|
src={article.file_url}
|
||||||
|
title={article.title}
|
||||||
|
class="h-[800px] w-full rounded-xl border shadow-inner"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
{:else if article.content_type === 'video'}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<video controls class="w-full rounded-xl border shadow-lg">
|
||||||
|
<source src={article.file_url} type={article.mime_type} />
|
||||||
|
Tu navegador no soporta videos.
|
||||||
|
</video>
|
||||||
|
<div class="rounded-lg border bg-muted/30 p-4">
|
||||||
|
<p class="text-sm font-medium">Información del archivo</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{article.mime_type}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center gap-6 rounded-2xl border border-dashed py-20"
|
||||||
|
>
|
||||||
|
<div class="rounded-full bg-primary/10 p-6">
|
||||||
|
<Download class="h-12 w-12 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-xl font-semibold">Archivo para descargar</h3>
|
||||||
|
<p class="text-muted-foreground truncate max-w-md">
|
||||||
|
Este archivo no tiene vista previa directa.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button href={article.file_url} download={article.title} class="px-8 shadow-lg">
|
||||||
|
<Download class="mr-2 h-4 w-4" />
|
||||||
|
Descargar ahora
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,11 @@
|
|||||||
Italic,
|
Italic,
|
||||||
Link as LinkIcon,
|
Link as LinkIcon,
|
||||||
List,
|
List,
|
||||||
Heading1,
|
Heading2,
|
||||||
Heading2
|
FileText,
|
||||||
|
Video as VideoIcon,
|
||||||
|
FileCode,
|
||||||
|
Upload
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
@@ -33,12 +36,23 @@
|
|||||||
let content = '';
|
let content = '';
|
||||||
let category = 'General';
|
let category = 'General';
|
||||||
let order = 0;
|
let order = 0;
|
||||||
let last_editor = 'Admin'; // Could pull from auth store
|
let last_editor = 'Admin';
|
||||||
|
let content_type = 'article';
|
||||||
|
let file_url = '';
|
||||||
|
let file_size: number | null = null;
|
||||||
|
let mime_type = '';
|
||||||
|
|
||||||
// Preview State
|
// Preview State
|
||||||
let showPreview = true;
|
let showPreview = true;
|
||||||
|
const HUB_MODE = import.meta.env.VITE_HUB_MODE === 'true';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// Proteccion: Solo el Hub puede editar/crear
|
||||||
|
if (!HUB_MODE) {
|
||||||
|
window.location.href = '/dashboard/help-center';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const uuid = $page.params.uuid as string;
|
const uuid = $page.params.uuid as string;
|
||||||
if (uuid === 'new') {
|
if (uuid === 'new') {
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -51,9 +65,13 @@
|
|||||||
if (article) {
|
if (article) {
|
||||||
title = article.title;
|
title = article.title;
|
||||||
slug = article.slug;
|
slug = article.slug;
|
||||||
content = article.content;
|
content = article.content || '';
|
||||||
category = article.category || 'General';
|
category = article.category || 'General';
|
||||||
order = article.order || 0;
|
order = article.order || 0;
|
||||||
|
content_type = article.content_type || 'article';
|
||||||
|
file_url = article.file_url || '';
|
||||||
|
file_size = article.file_size || null;
|
||||||
|
mime_type = article.mime_type || '';
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast.error('Error al cargar artículo: ' + e.message);
|
toast.error('Error al cargar artículo: ' + e.message);
|
||||||
@@ -65,7 +83,10 @@
|
|||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
processing = true;
|
processing = true;
|
||||||
try {
|
try {
|
||||||
const data = { title, slug, content, last_editor, category, order };
|
const data = {
|
||||||
|
title, slug, content, last_editor, category, order,
|
||||||
|
content_type, file_url, file_size, mime_type
|
||||||
|
};
|
||||||
|
|
||||||
// Auto-generate slug if missing
|
// Auto-generate slug if missing
|
||||||
if (!data.slug) {
|
if (!data.slug) {
|
||||||
@@ -128,6 +149,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAssetUpload(file: File) {
|
||||||
|
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-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');
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('Error al subir archivo: ' + e.message);
|
||||||
|
} finally {
|
||||||
|
processing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleDrop(e: DragEvent) {
|
function handleDrop(e: DragEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const files = e.dataTransfer?.files;
|
const files = e.dataTransfer?.files;
|
||||||
@@ -139,11 +184,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
function renderMarkdown(text: string) {
|
function renderMarkdown(text: string) {
|
||||||
// @ts-ignore
|
const html = marked.parse(text || '') as string;
|
||||||
return DOMPurify.sanitize(marked.parse(text || '') as string);
|
if (browser) {
|
||||||
|
return DOMPurify.sanitize(html);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -195,110 +241,203 @@
|
|||||||
<Label for="order">Orden</Label>
|
<Label for="order">Orden</Label>
|
||||||
<Input id="order" type="number" bind:value={order} />
|
<Input id="order" type="number" bind:value={order} />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid gap-2 pt-4 border-t">
|
||||||
|
<Label for="type">Tipo de Contenido</Label>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<option value="article">Artículo (Markdown)</option>
|
||||||
|
<option value="pdf">PDF</option>
|
||||||
|
<option value="video">Video</option>
|
||||||
|
<option value="document">Otro Documento</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Split View -->
|
<!-- Split View -->
|
||||||
<main class="flex flex-1 overflow-hidden">
|
<main class="flex flex-1 overflow-hidden">
|
||||||
<!-- Editor -->
|
{#if content_type === 'article'}
|
||||||
<div class="group relative flex flex-1 flex-col border-r">
|
<!-- Editor -->
|
||||||
<!-- Toolbar -->
|
<div class="group relative flex flex-1 flex-col border-r">
|
||||||
<div class="flex items-center gap-1 border-b bg-background p-2">
|
<!-- Toolbar -->
|
||||||
<Button
|
<div class="flex items-center gap-1 border-b bg-background p-2">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
size="icon"
|
||||||
onclick={() => insertText('**', '**')}
|
class="h-8 w-8"
|
||||||
title="Negrita"
|
onclick={() => insertText('**', '**')}
|
||||||
|
title="Negrita"
|
||||||
|
>
|
||||||
|
<Bold class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
onclick={() => insertText('*', '*')}
|
||||||
|
title="Cursiva"
|
||||||
|
>
|
||||||
|
<Italic class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div class="mx-1 h-4 w-px bg-border"></div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
onclick={() => insertText('# ')}
|
||||||
|
title="Título 1"
|
||||||
|
>
|
||||||
|
<Heading1 class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
onclick={() => insertText('## ')}
|
||||||
|
title="Título 2"
|
||||||
|
>
|
||||||
|
<Heading2 class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div class="mx-1 h-4 w-px bg-border"></div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
onclick={() => insertText('- ')}
|
||||||
|
title="Lista"
|
||||||
|
>
|
||||||
|
<List class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
onclick={() => insertText('[texto](url)')}
|
||||||
|
title="Enlace"
|
||||||
|
>
|
||||||
|
<LinkIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<label
|
||||||
|
class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
title="Subir Imagen"
|
||||||
|
>
|
||||||
|
<ImageIcon class="h-4 w-4" />
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
onchange={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
if (target.files?.length) handleImageUpload(target.files[0]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
id="markdown-editor"
|
||||||
|
bind:value={content}
|
||||||
|
class="w-full flex-1 resize-none bg-background p-8 font-mono text-sm leading-relaxed outline-none"
|
||||||
|
placeholder="# Empieza a escribir aquí..."
|
||||||
|
ondrop={handleDrop}
|
||||||
|
ondragover={(e) => e.preventDefault()}
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<!-- Dropping Hint -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 m-4 hidden items-center justify-center rounded-lg border-2 border-dashed border-primary bg-primary/10 opacity-0 transition-opacity group-hover:flex"
|
||||||
>
|
>
|
||||||
<Bold class="h-4 w-4" />
|
<p class="font-medium text-primary">Arrastra imágenes aquí</p>
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
onclick={() => insertText('*', '*')}
|
|
||||||
title="Cursiva"
|
|
||||||
>
|
|
||||||
<Italic class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div class="mx-1 h-4 w-px bg-border"></div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
onclick={() => insertText('# ')}
|
|
||||||
title="Título 1"
|
|
||||||
>
|
|
||||||
<Heading1 class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
onclick={() => insertText('## ')}
|
|
||||||
title="Título 2"
|
|
||||||
>
|
|
||||||
<Heading2 class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div class="mx-1 h-4 w-px bg-border"></div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
onclick={() => insertText('- ')}
|
|
||||||
title="Lista"
|
|
||||||
>
|
|
||||||
<List class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
onclick={() => insertText('[texto](url)')}
|
|
||||||
title="Enlace"
|
|
||||||
>
|
|
||||||
<LinkIcon class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<label
|
|
||||||
class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
title="Subir Imagen"
|
|
||||||
>
|
|
||||||
<ImageIcon class="h-4 w-4" />
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
class="hidden"
|
|
||||||
onchange={(e) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
if (target.files?.length) handleImageUpload(target.files[0]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea
|
<!-- Preview -->
|
||||||
id="markdown-editor"
|
{#if showPreview}
|
||||||
bind:value={content}
|
<div class="w-1/2 flex-1 overflow-y-auto border-l bg-muted/5 p-8">
|
||||||
class="w-full flex-1 resize-none bg-background p-8 font-mono text-sm leading-relaxed outline-none"
|
<div class="prose max-w-none prose-slate dark:prose-invert">
|
||||||
placeholder="# Empieza a escribir aquí..."
|
{@html renderMarkdown(content)}
|
||||||
ondrop={handleDrop}
|
</div>
|
||||||
ondragover={(e) => e.preventDefault()}
|
</div>
|
||||||
></textarea>
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<!-- Asset Upload View -->
|
||||||
|
<div class="flex flex-1 flex-col items-center justify-center bg-muted/5 p-12">
|
||||||
|
<div class="w-full max-w-2xl space-y-8 text-center">
|
||||||
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
{#if content_type === 'pdf'}
|
||||||
|
<div class="rounded-full bg-red-100 p-6 dark:bg-red-900/20">
|
||||||
|
<FileText class="h-16 w-16 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold">Configuración de PDF</h2>
|
||||||
|
{:else if content_type === 'video'}
|
||||||
|
<div class="rounded-full bg-blue-100 p-6 dark:bg-blue-900/20">
|
||||||
|
<VideoIcon class="h-16 w-16 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold">Configuración de Video</h2>
|
||||||
|
{:else}
|
||||||
|
<div class="rounded-full bg-primary/10 p-6">
|
||||||
|
<FileCode class="h-16 w-16 text-primary" />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Dropping Hint -->
|
{#if file_url}
|
||||||
<div
|
<div class="relative flex flex-col gap-4 rounded-xl border bg-card p-6 shadow-sm">
|
||||||
class="pointer-events-none absolute inset-0 m-4 hidden items-center justify-center rounded-lg border-2 border-dashed border-primary bg-primary/10 opacity-0 transition-opacity group-hover:flex"
|
<div class="flex items-center gap-4">
|
||||||
>
|
<div class="rounded bg-muted p-2">
|
||||||
<p class="font-medium text-primary">Arrastra imágenes aquí</p>
|
<FileCode class="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex-1 text-left">
|
||||||
|
<p class="font-medium">Archivo Cargado</p>
|
||||||
<!-- Preview -->
|
<p class="text-xs text-muted-foreground truncate">{file_url}</p>
|
||||||
{#if showPreview}
|
<p class="text-xs font-mono">
|
||||||
<div class="w-1/2 flex-1 overflow-y-auto border-l bg-muted/5 p-8">
|
{mime_type} • {(file_size ? (file_size / 1024 / 1024).toFixed(2) : '??')} MB
|
||||||
<div class="prose max-w-none prose-slate dark:prose-invert">
|
</p>
|
||||||
{@html renderMarkdown(content)}
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<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">
|
||||||
|
Cambiar Archivo
|
||||||
|
<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">
|
||||||
|
<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" />
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
onchange={(e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
if (target.files?.length) handleAssetUpload(target.files[0]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
9
start.sh
9
start.sh
@@ -77,8 +77,10 @@ if [ ! -f .env ]; then
|
|||||||
CENTRAL_URL=""
|
CENTRAL_URL=""
|
||||||
CLIENT_ID=""
|
CLIENT_ID=""
|
||||||
SPOKE_URLS=""
|
SPOKE_URLS=""
|
||||||
|
HUB_MODE="true"
|
||||||
else
|
else
|
||||||
echo -e "${GREEN}Configurando como CLIENTE...${NC}"
|
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
|
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/}
|
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
|
read -p "Ingrese el UUID de este cliente (presione Enter para generar uno): " CLIENT_ID
|
||||||
@@ -95,6 +97,12 @@ if [ ! -f .env ]; then
|
|||||||
sed -i "s|SPOKE_URLS=.*|SPOKE_URLS=$SPOKE_URLS|" .env
|
sed -i "s|SPOKE_URLS=.*|SPOKE_URLS=$SPOKE_URLS|" .env
|
||||||
sed -i "s|CLIENT_UUID=.*|CLIENT_UUID=$CLIENT_ID|" .env
|
sed -i "s|CLIENT_UUID=.*|CLIENT_UUID=$CLIENT_ID|" .env
|
||||||
sed -i "s|SYNC_SECRET_TOKEN=.*|SYNC_SECRET_TOKEN=$SYNC_TOKEN|" .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
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}⚠ .env.example no existe, creando .env básico${NC}"
|
echo -e "${YELLOW}⚠ .env.example no existe, creando .env básico${NC}"
|
||||||
cat > .env <<EOF
|
cat > .env <<EOF
|
||||||
@@ -115,6 +123,7 @@ CENTRAL_SERVER_URL=$CENTRAL_URL
|
|||||||
SPOKE_URLS=$SPOKE_URLS
|
SPOKE_URLS=$SPOKE_URLS
|
||||||
CLIENT_UUID=$CLIENT_ID
|
CLIENT_UUID=$CLIENT_ID
|
||||||
SYNC_SECRET_TOKEN=$SYNC_TOKEN
|
SYNC_SECRET_TOKEN=$SYNC_TOKEN
|
||||||
|
VITE_HUB_MODE=$HUB_MODE
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
echo -e "${GREEN}✓ Configuración de sincronización aplicada al .env${NC}"
|
echo -e "${GREEN}✓ Configuración de sincronización aplicada al .env${NC}"
|
||||||
|
|||||||
Reference in New Issue
Block a user