From bbe9a42691c7bf2bab9a0cedabfa755ec865a173 Mon Sep 17 00:00:00 2001 From: APAW Agent Sync Date: Thu, 14 May 2026 09:51:33 +0100 Subject: [PATCH] fix(perf): fix admin panel 30s load + add loader + abort controller 1. Replace serveStatic with Bun.file() to fix Content-Length: 0 bug that caused Nginx to wait 12-30s per file. 2. Add section loader (spinner + 'Cargando...') while sections load. 3. Add AbortController to cancel previous fetch when switching menus. 4. Add credentials: 'same-origin' to ensure cookies are sent. 5. Add error handling for empty responses and HTTP errors. Fixes: admin panel empty sections, 30s menu load, DOMException aborts. Refs: production server tenerifeprop.es --- public/admin.html | 30 +++++++++++++++++++++++++++++- public/js/admin.js | 31 ++++++++++++++++++++++++++++--- src/server/index.ts | 40 ++++++++++++++++++++-------------------- 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/public/admin.html b/public/admin.html index b2b296f..75b8d65 100644 --- a/public/admin.html +++ b/public/admin.html @@ -52,6 +52,31 @@ box-sizing: border-box; } + /* Section Loader */ + .section-loader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + gap: 16px; + } + .section-loader .loader-spinner { + width: 48px; + height: 48px; + border: 3px solid var(--card-border); + border-top-color: var(--primary); + border-radius: 50%; + animation: loader-spin 0.8s linear infinite; + } + .section-loader p { + color: var(--text-muted); + font-size: 14px; + } + @keyframes loader-spin { + to { transform: rotate(360deg); } + } + html, body { height: 100%; } @@ -1285,7 +1310,10 @@
- +
diff --git a/public/js/admin.js b/public/js/admin.js index 84c096f..2abbdcc 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -66,25 +66,50 @@ class AdminPanel { }) } + getLoader() { + return document.getElementById('section-loader') + } + + showLoader(show) { + const loader = this.getLoader() + if (loader) loader.style.display = show ? 'flex' : 'none' + } + async loadSection(section) { const content = document.getElementById('admin-content') if (!content) return + // Abort any ongoing fetch to prevent race conditions + if (this._abortController) this._abortController.abort() + this._abortController = new AbortController() + // Hide all sections content.querySelectorAll('.page-section').forEach(s => s.classList.remove('active')) // If section not loaded yet, fetch it let sec = document.getElementById(`section-${section}`) if (!sec) { + this.showLoader(true) try { - const res = await fetch(`/admin/${section}.html`) + const res = await fetch(`/admin/${section}.html`, { + signal: this._abortController.signal, + credentials: 'same-origin' + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) const html = await res.text() + if (!html || html.length < 50) throw new Error('Empty response') content.insertAdjacentHTML('beforeend', html) sec = document.getElementById(`section-${section}`) - if (!sec) return + if (!sec) throw new Error('Section not found in response') } catch (e) { - console.error(`Failed to load section: ${section}`, e) + if (e.name === 'AbortError') { + console.warn(`Section ${section} load aborted`) + } else { + console.error(`Failed to load section: ${section}`, e) + } return + } finally { + this.showLoader(false) } } sec.classList.add('active') diff --git a/src/server/index.ts b/src/server/index.ts index 362bace..24d380f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1794,29 +1794,29 @@ const adminHtmlAuthDisabled = async (c: any, next: any) => { } // Serve static files and SPA routes (clean URLs without .html) -// Admin component files - serve explicitly BEFORE the /admin route -app.get('/admin/sidebar.html', serveStatic({ path: './public/admin/sidebar.html' })) -app.get('/admin/topbar.html', serveStatic({ path: './public/admin/topbar.html' })) -app.get('/admin/dashboard.html', serveStatic({ path: './public/admin/dashboard.html' })) -app.get('/admin/properties.html', serveStatic({ path: './public/admin/properties.html' })) -app.get('/admin/leads.html', serveStatic({ path: './public/admin/leads.html' })) -app.get('/admin/testimonials.html', serveStatic({ path: './public/admin/testimonials.html' })) -app.get('/admin/faq.html', serveStatic({ path: './public/admin/faq.html' })) -app.get('/admin/services.html', serveStatic({ path: './public/admin/services.html' })) -app.get('/admin/settings.html', serveStatic({ path: './public/admin/settings.html' })) -app.get('/admin/users.html', serveStatic({ path: './public/admin/users.html' })) -app.get('/admin/analytics.html', serveStatic({ path: './public/admin/analytics.html' })) -app.get('/admin/traffic.html', serveStatic({ path: './public/admin/traffic.html' })) +// Admin component files - using Bun.file() directly to avoid serveStatic content-length bug +app.get('/admin/sidebar.html', async (c) => new Response(Bun.file('./public/admin/sidebar.html'))) +app.get('/admin/topbar.html', async (c) => new Response(Bun.file('./public/admin/topbar.html'))) +app.get('/admin/dashboard.html', async (c) => new Response(Bun.file('./public/admin/dashboard.html'))) +app.get('/admin/properties.html', async (c) => new Response(Bun.file('./public/admin/properties.html'))) +app.get('/admin/leads.html', async (c) => new Response(Bun.file('./public/admin/leads.html'))) +app.get('/admin/testimonials.html', async (c) => new Response(Bun.file('./public/admin/testimonials.html'))) +app.get('/admin/faq.html', async (c) => new Response(Bun.file('./public/admin/faq.html'))) +app.get('/admin/services.html', async (c) => new Response(Bun.file('./public/admin/services.html'))) +app.get('/admin/settings.html', async (c) => new Response(Bun.file('./public/admin/settings.html'))) +app.get('/admin/users.html', async (c) => new Response(Bun.file('./public/admin/users.html'))) +app.get('/admin/analytics.html', async (c) => new Response(Bun.file('./public/admin/analytics.html'))) +app.get('/admin/traffic.html', async (c) => new Response(Bun.file('./public/admin/traffic.html'))) -// SPA routes -app.get('/property/*', serveStatic({ path: './public/property.html' })) -app.get('/catalog', serveStatic({ path: './public/catalog.html' })) -app.get('/catalog.html', serveStatic({ path: './public/catalog.html' })) -app.get('/admin', serveStatic({ path: './public/admin.html' })) -app.get('/login', serveStatic({ path: './public/login.html' })) +// SPA routes - using Bun.file() for static files to avoid content-length bugs +app.get('/property/*', async (c) => new Response(Bun.file('./public/property.html'))) +app.get('/catalog', async (c) => new Response(Bun.file('./public/catalog.html'))) +app.get('/catalog.html', async (c) => new Response(Bun.file('./public/catalog.html'))) +app.get('/admin', async (c) => new Response(Bun.file('./public/admin.html'))) +app.get('/login', async (c) => new Response(Bun.file('./public/login.html'))) // Fallback to index.html for all other routes -app.get('*', serveStatic({ path: './public/index.html' })) +app.get('*', async (c) => new Response(Bun.file('./public/index.html'))) // Start server const port = parseInt(process.env.PORT || '8080')