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:
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user