diff --git a/backend/api/v1/modules/core/help_center/tasks.py b/backend/api/v1/modules/core/help_center/tasks.py index d069f94c..107df26d 100644 --- a/backend/api/v1/modules/core/help_center/tasks.py +++ b/backend/api/v1/modules/core/help_center/tasks.py @@ -244,8 +244,10 @@ def sync_from_hub_task(): # 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'): + # art_data contains the virtual fields because it was dumped via HelpArticleInDB + if "file_url" in art_data and art_data['file_url']: download_file_from_hub(art_data['file_url']) + sync_assets_from_content(art_data.get('content', '')) logger.info("Polling sync completed successfully.") diff --git a/backend/api/v1/modules/core/help_center/utils.py b/backend/api/v1/modules/core/help_center/utils.py index c6140e14..b5fe4c58 100644 --- a/backend/api/v1/modules/core/help_center/utils.py +++ b/backend/api/v1/modules/core/help_center/utils.py @@ -29,6 +29,7 @@ def download_file_from_hub(relative_path: str) -> bool: local_path = Path(clean_path) if local_path.exists(): + logger.info(f"File {clean_path} already exists, skipping download.") return True # Ensure directories exist @@ -38,7 +39,9 @@ def download_file_from_hub(relative_path: str) -> bool: # 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/', '')}" + # The file in backend is served usually under /api/uploads/... + # But clean_path is just "uploads/...". So the Hub route is base_url + "/" + clean_path + hub_file_url = f"{base_url}/{clean_path}" logger.info(f"Downloading asset from Hub: {hub_file_url} -> {local_path}") @@ -51,7 +54,7 @@ def download_file_from_hub(relative_path: str) -> bool: logger.info(f"Successfully downloaded {clean_path}") return True else: - logger.warning(f"Failed to download {clean_path}: Status {response.status_code}") + logger.warning(f"Failed to download {clean_path}: Status {response.status_code} URL: {hub_file_url}") return False except Exception as e: logger.error(f"Error downloading {clean_path}: {str(e)}") diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule new file mode 100644 index 00000000..6999c017 Binary files /dev/null and b/backend/celerybeat-schedule differ diff --git a/docker-compose.yml b/docker-compose.yml index 15835a1d..f15f8519 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -275,6 +275,7 @@ services: memory: 1G reservations: memory: 512M + # celery celery_worker: build: ./backend @@ -294,6 +295,10 @@ services: depends_on: - backend - valkey + volumes: + - ./backend:/app + - backend_cache:/app/__pycache__ + - backend_uploads:/app/uploads networks: - backend-net @@ -315,6 +320,10 @@ services: depends_on: - backend - valkey + volumes: + - ./backend:/app + - backend_cache:/app/__pycache__ + - backend_uploads:/app/uploads networks: - backend-net diff --git a/frontend/src/routes/dashboard/help-center/[uuid]/+page.svelte b/frontend/src/routes/dashboard/help-center/[uuid]/+page.svelte index e9f78463..0cee42c6 100644 --- a/frontend/src/routes/dashboard/help-center/[uuid]/+page.svelte +++ b/frontend/src/routes/dashboard/help-center/[uuid]/+page.svelte @@ -16,6 +16,25 @@ import { marked } from 'marked'; import DOMPurify from 'dompurify'; + // Helper to resolve an API path from the DB (e.g., /api/uploads/...) to a full URL pointing to the local backend. + function resolveAssetUrl(url: string | undefined): string { + try { + if (!url) return ''; + if (url.startsWith('http://') || url.startsWith('https://')) return url; + + // Fallback to empty string if VITE_API_URL is missing + // @ts-ignore + const apiUrl = import.meta.env.VITE_API_URL || ''; + const base = apiUrl.replace(/\/$/, ''); + const cleanUrl = url.replace(/^\/api\//, '/'); + const finalUrl = `${base}${cleanUrl}`; + return finalUrl; + } catch (e) { + console.error('[SSR DEBUG] Error in resolveAssetUrl:', e); + return ''; + } + } + let article: HelpArticle | null = null; // Removed $state() as it's not reactive by itself in Svelte 5 standard store usage unless wrapped let loading = true; // Use standard value, reactive via variable assignment in Svelte 5 runes if enabled let error: string | null = null; @@ -60,16 +79,32 @@ import { browser } from '$app/environment'; function renderMarkdown(content: string) { - const rawHtml = marked.parse(content || '') as string; - // Add IDs to headers for TOC navigation - const htmlWithIds = rawHtml.replace(/(.*?)<\/h\1>/g, (match, level, text) => { - const id = text.toLowerCase().replace(/[^\w]+/g, '-'); - return `${text}`; - }); - if (browser) { - return DOMPurify.sanitize(htmlWithIds); + if (!content) return ''; + try { + const rawHtml = marked.parse(content) as string; + // Add IDs to headers for TOC navigation + let htmlWithIds = rawHtml.replace(/(.*?)<\/h\1>/g, (match, level, text) => { + const id = text.toLowerCase().replace(/[^\w]+/g, '-'); + return `${text}`; + }); + + // Rewrite image src and link hrefs to point to the local backend + htmlWithIds = htmlWithIds.replace(/src="([^"]+)"/g, (match, src) => { + return `src="${resolveAssetUrl(src)}"`; + }); + htmlWithIds = htmlWithIds.replace(/href="(\/api\/uploads\/[^"]+)"/g, (match, href) => { + return `href="${resolveAssetUrl(href)}"`; + }); + + if (browser && typeof window !== 'undefined') { + // DOMPurify only works in the browser + return DOMPurify.sanitize(htmlWithIds); + } + return htmlWithIds; // SSR returns raw HTML (or could use isomorphic-dompurify) + } catch (e) { + console.error('Markdown parsing error:', e); + return ''; } - return htmlWithIds; } function scrollToHeader(id: string) { @@ -152,11 +187,20 @@
- - @@ -164,7 +208,7 @@
@@ -172,7 +216,7 @@ {:else if article.content_type === 'video'}
@@ -193,7 +237,11 @@ Este archivo no tiene vista previa directa.

- diff --git a/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte b/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte index 103d8404..2455fea8 100644 --- a/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte +++ b/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte @@ -27,6 +27,19 @@ import { marked } from 'marked'; import DOMPurify from 'dompurify'; + // Helper to resolve an API path from the DB (e.g., /api/uploads/...) to a full URL pointing to the local backend. + function resolveAssetUrl(url: string | undefined): string { + if (!url) return ''; + if (url.startsWith('http://') || url.startsWith('https://')) return url; + + // Fallback to empty string if VITE_API_URL is missing + // @ts-ignore + const apiUrl = import.meta.env.VITE_API_URL || ''; + const base = apiUrl.replace(/\/$/, ''); + const cleanUrl = url.replace(/^\/api\//, '/'); + return `${base}${cleanUrl}`; + } + let article = $state(null); let loading = $state(true); let processing = $state(false); @@ -206,11 +219,26 @@ } function renderMarkdown(text: string) { - const html = marked.parse(text || '') as string; - if (browser) { - return DOMPurify.sanitize(html); + if (!text) return ''; + try { + const html = marked.parse(text) as string; + + // Rewrite image src and link hrefs to point to the local backend + let resolvedHtml = html.replace(/src="([^"]+)"/g, (match, src) => { + return `src="${resolveAssetUrl(src)}"`; + }); + resolvedHtml = resolvedHtml.replace(/href="(\/api\/uploads\/[^"]+)"/g, (match, href) => { + return `href="${resolveAssetUrl(href)}"`; + }); + + if (browser && typeof window !== 'undefined') { + return DOMPurify.sanitize(resolvedHtml); + } + return resolvedHtml; + } catch (e) { + console.error("Markdown parsing error in editor:", e); + return ''; } - return html; } @@ -359,8 +387,8 @@ 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()} + ondrop={(e: DragEvent) => handleDrop(e)} + ondragover={(e: DragEvent) => e.preventDefault()} > @@ -418,7 +446,7 @@
-