Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5222953dd |
@@ -9,6 +9,13 @@ so you can always tell what a deployment is actually serving.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.4.2] - 2026-09-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Stylesheet and scripts are referenced with `?v=<version>`, so a new release can no longer be
|
||||||
|
served with a cached old `app.css` (which made the score keeper page render unstyled on
|
||||||
|
phones after 0.4.0). Versioned assets are cached for a year; unversioned for a minute.
|
||||||
|
|
||||||
## [0.4.1] - 2026-09-03
|
## [0.4.1] - 2026-09-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"name":"courtside","type":"module","version":"0.4.1","scripts":{"test":"node --test test/*.test.js","sim":"node simulate.js","start":"node --disable-warning=ExperimentalWarning server/index.js","dev":"node --disable-warning=ExperimentalWarning --watch server/index.js","demo":"node --disable-warning=ExperimentalWarning scripts/seed-demo.js","smoke":"bash test/smoke.sh"},"description":"Self-hosted volleyball tournament desk: one QR code from check-in to live scores","main":"server/index.js","directories":{"test":"test"},"keywords":[],"author":"","license":"MIT","dependencies":{"qrcode":"^1.5.4","ws":"^8.21.3"},"engines":{"node":">=22.13"},"repository":{"type":"git","url":"https://gitea.cloudfreeiot.com/bkvargyas/courtside.git"}}
|
{"name":"courtside","type":"module","version":"0.4.2","scripts":{"test":"node --test test/*.test.js","sim":"node simulate.js","start":"node --disable-warning=ExperimentalWarning server/index.js","dev":"node --disable-warning=ExperimentalWarning --watch server/index.js","demo":"node --disable-warning=ExperimentalWarning scripts/seed-demo.js","smoke":"bash test/smoke.sh"},"description":"Self-hosted volleyball tournament desk: one QR code from check-in to live scores","main":"server/index.js","directories":{"test":"test"},"keywords":[],"author":"","license":"MIT","dependencies":{"qrcode":"^1.5.4","ws":"^8.21.3"},"engines":{"node":">=22.13"},"repository":{"type":"git","url":"https://gitea.cloudfreeiot.com/bkvargyas/courtside.git"}}
|
||||||
+4
-2
@@ -48,14 +48,16 @@ export const send = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MIME = { '.css': 'text/css', '.js': 'text/javascript', '.png': 'image/png', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.webmanifest': 'application/manifest+json', '.json': 'application/json' };
|
const MIME = { '.css': 'text/css', '.js': 'text/javascript', '.png': 'image/png', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.webmanifest': 'application/manifest+json', '.json': 'application/json' };
|
||||||
export async function serveStatic(res, root, urlPath) {
|
export async function serveStatic(res, root, urlPath, versioned = false) {
|
||||||
const p = normalize(join(root, urlPath));
|
const p = normalize(join(root, urlPath));
|
||||||
if (!p.startsWith(root)) throw new HttpError(404, 'Not found');
|
if (!p.startsWith(root)) throw new HttpError(404, 'Not found');
|
||||||
try {
|
try {
|
||||||
const s = await stat(p);
|
const s = await stat(p);
|
||||||
if (!s.isFile()) throw new HttpError(404, 'Not found');
|
if (!s.isFile()) throw new HttpError(404, 'Not found');
|
||||||
const body = await readFile(p);
|
const body = await readFile(p);
|
||||||
res.writeHead(200, { 'content-type': MIME[extname(p)] ?? 'application/octet-stream', 'cache-control': 'public, max-age=300' });
|
// Assets are referenced with ?v=<version>, so a new release is a new URL: cache the versioned
|
||||||
|
// URL for a year, and keep unversioned requests short-lived.
|
||||||
|
res.writeHead(200, { 'content-type': MIME[extname(p)] ?? 'application/octet-stream', 'cache-control': versioned ? 'public, max-age=31536000, immutable' : 'public, max-age=60, must-revalidate' });
|
||||||
res.end(body);
|
res.end(body);
|
||||||
} catch (e) { if (e instanceof HttpError) throw e; throw new HttpError(404, 'Not found'); }
|
} catch (e) { if (e instanceof HttpError) throw e; throw new HttpError(404, 'Not found'); }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ const staticRoot = join(here, 'public');
|
|||||||
const server = createServer(async (req, res) => {
|
const server = createServer(async (req, res) => {
|
||||||
const url = new URL(req.url, 'http://x');
|
const url = new URL(req.url, 'http://x');
|
||||||
try {
|
try {
|
||||||
if (url.pathname.startsWith('/static/')) return await serveStatic(res, staticRoot, url.pathname.slice('/static'.length));
|
if (url.pathname.startsWith('/static/')) return await serveStatic(res, staticRoot, url.pathname.slice('/static'.length), url.searchParams.has('v'));
|
||||||
const m = router.match(req.method, url.pathname);
|
const m = router.match(req.method, url.pathname);
|
||||||
if (!m) throw new HttpError(404, 'Not found');
|
if (!m) throw new HttpError(404, 'Not found');
|
||||||
await m.handler(req, res, m.params);
|
await m.handler(req, res, m.params);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function layout({ title, body, state = null, script = null, bodyClass = '
|
|||||||
<link rel="manifest" href="/static/manifest.webmanifest">
|
<link rel="manifest" href="/static/manifest.webmanifest">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Source+Sans+3:wght@400;600&family=JetBrains+Mono:wght@500&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Source+Sans+3:wght@400;600&family=JetBrains+Mono:wght@500&display=swap">
|
||||||
<link rel="stylesheet" href="/static/app.css">
|
<link rel="stylesheet" href="/static/app.css?v=${VERSION}">
|
||||||
</head>
|
</head>
|
||||||
<body class="${esc(bodyClass)}">
|
<body class="${esc(bodyClass)}">
|
||||||
<header class="top">
|
<header class="top">
|
||||||
@@ -25,7 +25,7 @@ ${body}
|
|||||||
</main>
|
</main>
|
||||||
<footer class="foot">Courtside v${VERSION} · self-hosted tournament desk · <a href="https://gitea.cloudfreeiot.com/bkvargyas/courtside">source</a> · <a href="/admin">organizer desk</a></footer>
|
<footer class="foot">Courtside v${VERSION} · self-hosted tournament desk · <a href="https://gitea.cloudfreeiot.com/bkvargyas/courtside">source</a> · <a href="/admin">organizer desk</a></footer>
|
||||||
${state ? `<script id="state" type="application/json">${JSON.stringify(state).replace(/</g, '\\u003c')}</script>` : ''}
|
${state ? `<script id="state" type="application/json">${JSON.stringify(state).replace(/</g, '\\u003c')}</script>` : ''}
|
||||||
${script ? `<script src="${esc(script)}" defer></script>` : ''}
|
${script ? `<script src="${esc(script)}?v=${VERSION}" defer></script>` : ''}
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user