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:
@@ -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, () => {
|
||||
|
||||
Reference in New Issue
Block a user