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
This commit is contained in:
APAW Agent Sync
2026-05-14 09:51:33 +01:00
parent 578ea18e6b
commit bbe9a42691
3 changed files with 77 additions and 24 deletions

View File

@@ -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 @@
<main class="page-content">
<div id="admin-content">
<!-- Sections loaded dynamically via JavaScript -->
<div id="section-loader" class="section-loader" style="display:none">
<div class="loader-spinner"></div>
<p>Cargando...</p>
</div>
</div>
</main>
</div>

View File

@@ -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')

View File

@@ -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')