Three WebUI cold-load wins:
1. DELETED containers/libreportal/frontend/js/components/config/config-manager-old.js
66 KB / 68189 bytes. Zero references anywhere in source or deployed
tree (confirmed via grep across containers/libreportal/). Pure dead
code from a previous refactor — removed.
2. ADDED `compression` middleware (defensive require)
Gzip-compresses JS/CSS/HTML/JSON responses. Typical ~70 % wire-size
reduction → the 1.7 MB cold-load drops to ~500 KB. New package.json
dependency; container's node_modules is baked into the image so the
require is wrapped in try/catch to degrade silently until the image
is next rebuilt (libreportal app install libreportal, or a full
deploy). Once active: free wire-size win on every response.
3. ADDED static cache headers via staticOptions on express.static
- JS/CSS/icons: Cache-Control: max-age=3600 + ETag
(1h browser cache, cheap 304 revalidation after)
- HTML files: Cache-Control: no-cache + ETag
(always revalidates so SPA shell updates land
immediately after a deploy; 304 if unchanged)
Repeat navigation in the same browser session skips ~25 script-tag
round-trips entirely.
Net effect once compression deploys:
- Cold load: 1.7 MB → ~500 KB on the wire (~70 % shrink)
- Warm load: 25 conditional requests → 0 (served from cache for 1h)
- Deploy lands: HTML revalidates immediately, JS/CSS picks up after 1h
or hard refresh
Phase B (defer non-critical scripts via SPA loadScript) and Phase C
(rebuild image / split the bind-mount story for node_modules) come
next; this commit is the safe Phase A foundation.
Signed-off-by: librelad <librelad@digitalangels.vip>
78 lines
3.1 KiB
JavaScript
Executable File
78 lines
3.1 KiB
JavaScript
Executable File
const express = require('express');
|
||
const path = require('path');
|
||
const cookieParser = require('cookie-parser');
|
||
const config = require('./config.js');
|
||
const { verifyToken } = require('./auth.js');
|
||
|
||
// compression is a new dependency (added to package.json). The Docker image
|
||
// bakes node_modules at build time and routes/utils/server.js are bind-mounted
|
||
// in compose.yml — but node_modules is NOT bind-mounted, so a "quick" deploy
|
||
// (cp + restart) hits the old image without compression installed. We require
|
||
// it defensively: present after the next image rebuild → ~70 % wire-size
|
||
// reduction; absent → degrade silently to the previous uncompressed behaviour.
|
||
let compression = null;
|
||
try { compression = require('compression'); } catch (_) {}
|
||
|
||
function requireAuth(req, res, next) {
|
||
const token = req.cookies?.libreportal_token;
|
||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||
const payload = verifyToken(token);
|
||
if (!payload) return res.status(401).json({ error: 'Token expired or invalid' });
|
||
req.user = payload;
|
||
next();
|
||
}
|
||
|
||
// Prevent the browser from caching authenticated /data/* responses so they're
|
||
// not retained after logout or persisted to disk caches.
|
||
function noStore(req, res, next) {
|
||
res.setHeader('Cache-Control', 'no-store');
|
||
next();
|
||
}
|
||
|
||
// Static-asset options:
|
||
// - 1h maxAge + ETag on JS/CSS/icons so repeated nav skips a network
|
||
// round-trip per file. ~25 script tags × ~5ms RTT each adds up otherwise.
|
||
// - HTML files get Cache-Control: no-cache (still uses ETag, so revalidation
|
||
// is cheap, but new deploys land immediately without waiting for cache
|
||
// expiry — the SPA shell is the file most likely to change between deploys).
|
||
// - dotfiles='ignore' so .auth.json is never served.
|
||
const staticOptions = {
|
||
maxAge: '1h',
|
||
etag: true,
|
||
dotfiles: 'ignore',
|
||
setHeaders: (res, filePath) => {
|
||
if (filePath.endsWith('.html')) {
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
}
|
||
}
|
||
};
|
||
|
||
function setup(app) {
|
||
// Gzip-compress responses. JS/CSS/HTML/JSON typically shrink ~70 %, so the
|
||
// 1.7 MB of static assets the SPA loads on a cold cache drop to ~500 KB on
|
||
// the wire. compression defaults skip already-compressed types (images,
|
||
// gzipped tarballs) and small responses (<1 KB). Defensive — no-op if the
|
||
// module isn't installed (image not yet rebuilt with the new dep).
|
||
if (compression) app.use(compression());
|
||
|
||
app.use(express.json());
|
||
app.use(cookieParser());
|
||
|
||
// Block MIME sniffing on every response.
|
||
app.use((req, res, next) => {
|
||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||
next();
|
||
});
|
||
|
||
// /data/* requires auth. express.static doesn't generate directory listings,
|
||
// so the only way to read anything is to know an exact path. noStore wins
|
||
// over staticOptions' maxAge for this prefix — auth-sensitive content
|
||
// should never be cached.
|
||
app.use('/data', requireAuth, noStore, express.static(path.join(config.FRONTEND_PATH, 'data')));
|
||
|
||
// All other static assets (js, css, icons, html partials, index.html) remain public.
|
||
app.use(express.static(config.FRONTEND_PATH, staticOptions));
|
||
}
|
||
|
||
module.exports = { setup, requireAuth };
|