Update 0.1.8 #2

Merged
jhoogie merged 3 commits from update-0.1.8 into main 2026-09-09 12:17:35 +02:00
7 changed files with 107 additions and 12 deletions
+26
View File
@@ -107,6 +107,9 @@ docker compose pull && docker compose up -d
docker compose down -v && 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 ### PostgreSQL versie-upgrade
Bij een upgrade van PostgreSQL (bijv. 16 → 18) is de bestaande data niet compatibel. Stappen: 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 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 ## Update-notificaties
+22
View File
@@ -1,5 +1,27 @@
# Changelog # 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
- `release.ps1 -DryRun` schrijft de frontend-versie niet meer echt weg: een
droogloop meldde de sync wél en voerde hem óók uit, waardoor er na een
`-DryRun` een gewijzigde `frontend/package.json` bleef liggen
### 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 ## [0.1.7] - 2026-09-08
### Gewijzigd ### Gewijzigd
+20 -7
View File
@@ -5,7 +5,7 @@ import rateLimit from "express-rate-limit";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import pinoHttp from "pino-http"; import pinoHttp from "pino-http";
import { existsSync } from "fs"; import { existsSync } from "fs";
import { join } from "path"; import { extname, join, relative, sep } from "path";
import { config } from "./config/index.js"; import { config } from "./config/index.js";
import { logger } from "./logger.js"; import { logger } from "./logger.js";
import { initDb } from "./db.js"; import { initDb } from "./db.js";
@@ -73,21 +73,34 @@ const indexHtml = join(config.STATIC_DIR, "index.html");
export const servesFrontend = existsSync(indexHtml); export const servesFrontend = existsSync(indexHtml);
if (servesFrontend) { if (servesFrontend) {
// Assets hebben een hash in de naam en mogen lang gecached worden; // Alleen de bundels in /assets krijgen bij elke build een nieuwe, gehashte
// index.html juist nooit, anders blijft een oude build hangen. // 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, { app.use(express.static(config.STATIC_DIR, {
index: false, index: false,
setHeaders(res, path) { setHeaders(res, bestandspad) {
res.setHeader("Cache-Control", path.endsWith(".html") const relatief = relative(config.STATIC_DIR, bestandspad).split(sep).join("/");
? "no-store, no-cache, must-revalidate" if (bestandspad.endsWith(".html")) {
: "public, max-age=31536000, immutable"); 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 // SPA-fallback: alle overige GET-routes naar index.html. Pathloze middleware
// in plaats van app.get("*") — dat patroon is in Express 5 ongeldig. // 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) => { app.use((req, res, next) => {
if (req.method !== "GET" && req.method !== "HEAD") return 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.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
res.sendFile(indexHtml); res.sendFile(indexHtml);
}); });
+28
View File
@@ -13,6 +13,7 @@ import { join } from "node:path";
const INDEX_MARKER = "<div id=\"root\"></div>"; const INDEX_MARKER = "<div id=\"root\"></div>";
const ASSET_BODY = "console.log('vibefinance');"; const ASSET_BODY = "console.log('vibefinance');";
const FAVICON_BODY = "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
let server; let server;
let basis; let basis;
@@ -23,6 +24,7 @@ before(async () => {
mkdirSync(join(staticDir, "assets")); mkdirSync(join(staticDir, "assets"));
writeFileSync(join(staticDir, "index.html"), `<!DOCTYPE html><html><body>${INDEX_MARKER}</body></html>`); writeFileSync(join(staticDir, "index.html"), `<!DOCTYPE html><html><body>${INDEX_MARKER}</body></html>`);
writeFileSync(join(staticDir, "assets", "index-abc12345.js"), ASSET_BODY); writeFileSync(join(staticDir, "assets", "index-abc12345.js"), ASSET_BODY);
writeFileSync(join(staticDir, "favicon.svg"), FAVICON_BODY);
process.env.NODE_ENV = "test"; process.env.NODE_ENV = "test";
process.env.STATIC_DIR = staticDir; process.env.STATIC_DIR = staticDir;
@@ -93,6 +95,32 @@ describe("Frontend serveren", () => {
assert.equal(await res.text(), ASSET_BODY); assert.equal(await res.text(), ASSET_BODY);
assert.match(res.headers.get("cache-control") ?? "", /immutable/); 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", () => { describe("Security headers", () => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "vibefinance-frontend", "name": "vibefinance-frontend",
"version": "0.1.6", "version": "0.1.8",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "vibefinance", "name": "vibefinance",
"version": "0.1.7", "version": "0.1.8",
"description": "VibeFinance — persoonlijk vermogensbeheer", "description": "VibeFinance — persoonlijk vermogensbeheer",
"private": true, "private": true,
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance", "repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
+6
View File
@@ -53,13 +53,19 @@ if ($appVersion -match "-dev$") {
} }
# ── Versie synchroniseren naar frontend/package.json ────────────────────── # ── Versie synchroniseren naar frontend/package.json ──────────────────────
# Een droogloop schrijft hier niet: anders laat -DryRun een gewijzigd bestand
# achter, en tagt de echte release daarna een vervuilde working tree.
$frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json" $frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json"
$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json $frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json
if ($frontendPkg.version -ne $appVersion) { if ($frontendPkg.version -ne $appVersion) {
if ($DryRun) {
Write-Host "[DRY-RUN] Versie naar frontend synchroniseren: $($frontendPkg.version) -> $appVersion" -ForegroundColor Yellow
} else {
$frontendPkg.version = $appVersion $frontendPkg.version = $appVersion
[System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10)) [System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10))
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green
} }
}
$gitTag = "$appVersion" $gitTag = "$appVersion"