librelad 011737455b perf(webui): delete dead config-manager-old.js + gzip + cache headers (Phase A)
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>
2026-05-26 22:10:59 +01:00

78 lines
3.1 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };