ui: keep sidebar controls visible during playback; move collapse to header

- PlayerBar no longer overlaps the sidebar on desktop (offsets by sidebar width),
  so Settings/collapse stay reachable while playing.
- Sidebar collapse toggle moved onto the header logo icon; remove bottom button.
- Prod server: catch-all proxy so any non-SPA, non-asset request is forwarded to
  ABS — only Shelfless needs to be exposed, ABS stays internal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-02 21:00:49 +02:00
parent 757a6fab63
commit e570390902
3 changed files with 46 additions and 32 deletions

View File

@@ -85,30 +85,37 @@ function sendFile(res, filePath, status = 200) {
createReadStream(filePath).pipe(res)
}
function serveStatic(req, res) {
// Strip query, prevent path traversal, default to index.html (SPA fallback).
const urlPath = decodeURIComponent((req.url || '/').split('?')[0])
// Resolve a request path to an existing static file inside dist/, or null.
function resolveStaticFile(url) {
const urlPath = decodeURIComponent((url || '/').split('?')[0])
const safe = normalize(urlPath).replace(/^(\.\.[/\\])+/, '')
let filePath = join(DIST, safe)
if (!filePath.startsWith(DIST)) {
res.statusCode = 403
return res.end('Forbidden')
}
if (existsSync(filePath) && statSync(filePath).isFile()) {
return sendFile(res, filePath)
}
// SPA fallback
const indexFile = join(DIST, 'index.html')
if (existsSync(indexFile)) return sendFile(res, indexFile)
res.statusCode = 404
res.end('Not found')
const filePath = join(DIST, safe)
if (!filePath.startsWith(DIST)) return null
if (existsSync(filePath) && statSync(filePath).isFile()) return filePath
return null
}
const INDEX_HTML = join(DIST, 'index.html')
const server = http.createServer((req, res) => {
const url = req.url || '/'
// 1) Known ABS paths → forward upstream.
if (isProxied(url)) return proxy(req, res)
return serveStatic(req, res)
// 2) An existing static asset of the SPA → serve it.
const file = resolveStaticFile(url)
if (file) return sendFile(res, file)
// 3) A browser navigating to a client-side route → serve the SPA shell.
const accept = String(req.headers.accept || '')
if (req.method === 'GET' && accept.includes('text/html') && existsSync(INDEX_HTML)) {
return sendFile(res, INDEX_HTML)
}
// 4) Anything else (any other API/data request) → forward to ABS, so the real
// server never needs to be exposed on its own domain.
return proxy(req, res)
})
server.listen(PORT, HOST, () => {