diff --git a/BEHEER.md b/BEHEER.md index 193ade9..3aeac79 100644 --- a/BEHEER.md +++ b/BEHEER.md @@ -107,6 +107,9 @@ docker compose pull && docker compose up -d docker compose down -v && docker compose up -d ``` +Pullen vereist een registry-login op die host — zie +[Pullen op een deploy-machine](#pullen-op-een-deploy-machine). + ### PostgreSQL versie-upgrade Bij een upgrade van PostgreSQL (bijv. 16 → 18) is de bestaande data niet compatibel. Stappen: @@ -302,6 +305,29 @@ Controleren wat er is opgeslagen: docker-credential-desktop list ``` +### Pullen op een deploy-machine + +Het package is privé, dus ook een **pull** vereist een login — anoniem geeft +`401 Unauthorized` (`docker compose pull` meldt dan "no basic auth credentials" +of "pull access denied"). Eenmalig op de Docker-host: + +```bash +docker login vibehoogie.duckdns.org +# gebruikersnaam: je Gitea-account +# wachtwoord: access token met scope read:package +``` + +Daarna werkt `docker compose pull && docker compose up -d`. De login komt in +`~/.docker/config.json` van de gebruiker die het commando uitvoert — doe het dus +als dezelfde gebruiker (bij `sudo docker` is dat root). Log in op +`vibehoogie.duckdns.org` **zonder** `:3000`. + +Controleren zonder te pullen: + +```bash +docker manifest inspect vibehoogie.duckdns.org/vibe/vibefinance-app:latest +``` + --- ## Update-notificaties diff --git a/Changelog.md b/Changelog.md index 4b2158c..becc8c3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,24 @@ # Changelog +## [0.1.8] - 2026-09-09 + +### Gewijzigd +- `BEHEER.md` beschrijft de registry-login die een deploy-machine nodig heeft om + te kunnen pullen: het package is privé, dus ook `docker compose pull` vraagt + een token (`read:package`) — anoniem geeft de registry `401 Unauthorized` +- Testsuite dekt nu het serveren van de favicon, de cache-headers daarop en de + 404 op ontbrekende bestanden + +### Opgelost +- De favicon bleef weg in de browser: `favicon.svg` en `icons/*` hebben geen + hash in hun naam, maar kregen wel een jaar `immutable` mee — een eenmaal + verkeerd gecacht antwoord revalideerde daardoor nooit meer. Alleen de + gehashte bundels in `/assets` houden die cache; de rest revalideert +- `/favicon.ico` en `/apple-touch-icon.png` gaven via de SPA-fallback + `index.html` terug met status 200 en `text/html` — de browser kreeg HTML waar + hij een icoon verwachtte en toonde stil niets, zonder zichtbare fout. Paden + met een bestandsextensie geven nu een echte 404 + ## [0.1.7] - 2026-09-08 ### Gewijzigd diff --git a/backend/src/server.js b/backend/src/server.js index 2033c41..20685f4 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -5,7 +5,7 @@ import rateLimit from "express-rate-limit"; import cookieParser from "cookie-parser"; import pinoHttp from "pino-http"; import { existsSync } from "fs"; -import { join } from "path"; +import { extname, join, relative, sep } from "path"; import { config } from "./config/index.js"; import { logger } from "./logger.js"; import { initDb } from "./db.js"; @@ -73,21 +73,34 @@ const indexHtml = join(config.STATIC_DIR, "index.html"); export const servesFrontend = existsSync(indexHtml); if (servesFrontend) { - // Assets hebben een hash in de naam en mogen lang gecached worden; - // index.html juist nooit, anders blijft een oude build hangen. + // Alleen de bundels in /assets krijgen bij elke build een nieuwe, gehashte + // naam — die mogen een jaar blijven staan. Bestanden met een vaste naam + // (favicon.svg, icons/*) moeten revalideren: met "immutable" blijft een + // eenmaal verkeerd gecacht antwoord een jaar hangen, ook na een reload. app.use(express.static(config.STATIC_DIR, { index: false, - setHeaders(res, path) { - res.setHeader("Cache-Control", path.endsWith(".html") - ? "no-store, no-cache, must-revalidate" - : "public, max-age=31536000, immutable"); + setHeaders(res, bestandspad) { + const relatief = relative(config.STATIC_DIR, bestandspad).split(sep).join("/"); + if (bestandspad.endsWith(".html")) { + res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); + } else if (relatief.startsWith("assets/")) { + res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); + } else { + res.setHeader("Cache-Control", "public, max-age=0, must-revalidate"); + } }, })); // SPA-fallback: alle overige GET-routes naar index.html. Pathloze middleware // in plaats van app.get("*") — dat patroon is in Express 5 ongeldig. + // + // Paden met een extensie vragen om een bestand (/favicon.ico, /robots.txt) + // en krijgen een echte 404. Anders levert zo'n verzoek index.html op met + // status 200 en content-type text/html: de browser krijgt HTML waar hij een + // icoon verwacht en toont dan niets, zonder zichtbare fout. app.use((req, res, next) => { if (req.method !== "GET" && req.method !== "HEAD") return next(); + if (extname(req.path)) return next(); res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); res.sendFile(indexHtml); }); diff --git a/backend/test/app.test.js b/backend/test/app.test.js index 6ee90fe..9e846ca 100644 --- a/backend/test/app.test.js +++ b/backend/test/app.test.js @@ -13,6 +13,7 @@ import { join } from "node:path"; const INDEX_MARKER = "
"; const ASSET_BODY = "console.log('vibefinance');"; +const FAVICON_BODY = ""; let server; let basis; @@ -23,6 +24,7 @@ before(async () => { mkdirSync(join(staticDir, "assets")); writeFileSync(join(staticDir, "index.html"), `${INDEX_MARKER}`); writeFileSync(join(staticDir, "assets", "index-abc12345.js"), ASSET_BODY); + writeFileSync(join(staticDir, "favicon.svg"), FAVICON_BODY); process.env.NODE_ENV = "test"; process.env.STATIC_DIR = staticDir; @@ -93,6 +95,32 @@ describe("Frontend serveren", () => { assert.equal(await res.text(), ASSET_BODY); assert.match(res.headers.get("cache-control") ?? "", /immutable/); }); + + it("favicon wordt geserveerd als svg", async () => { + const res = await fetch(`${basis}/favicon.svg`); + assert.equal(res.status, 200); + assert.match(res.headers.get("content-type") ?? "", /image\/svg\+xml/); + assert.equal(await res.text(), FAVICON_BODY); + }); + + // De naam bevat geen hash, dus een jaar "immutable" zou een oude of + // verkeerde versie onhoudbaar lang in de browsercache vastzetten. + it("favicon wordt niet immutable gecached", async () => { + const res = await fetch(`${basis}/favicon.svg`); + const cache = res.headers.get("cache-control") ?? ""; + assert.doesNotMatch(cache, /immutable/); + assert.match(cache, /must-revalidate/); + }); + + // Anders krijgt de browser index.html met status 200 en content-type + // text/html waar hij een icoon verwacht, en toont hij stil niets. + it("ontbrekend bestand geeft 404 in plaats van index.html", async () => { + for (const pad of ["/favicon.ico", "/apple-touch-icon.png"]) { + const res = await fetch(`${basis}${pad}`); + assert.equal(res.status, 404, pad); + assert.doesNotMatch(await res.text(), new RegExp(INDEX_MARKER), pad); + } + }); }); describe("Security headers", () => { diff --git a/frontend/package.json b/frontend/package.json index 9333fc7..f5a636b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "vibefinance-frontend", - "version": "0.1.6", + "version": "0.1.8-dev", "private": true, "type": "module", "scripts": { diff --git a/package.json b/package.json index a5a7471..ce92d31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibefinance", - "version": "0.1.7", + "version": "0.1.8", "description": "VibeFinance — persoonlijk vermogensbeheer", "private": true, "repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",