7 Commits
13 changed files with 201 additions and 17 deletions
+2 -1
View File
@@ -6,7 +6,8 @@ frontend/dist/
# Omgevingsvariabelen (gebruik .env.example als sjabloon)
.env
.env.local
.env.*
!.env.example
# Logs en rommel
*.log
+62
View File
@@ -95,6 +95,42 @@ $env:GITEA_TOKEN = "<token>"
Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde
tag op een Raspberry Pi en op een x86-server werkt.
### Tokens niet elke sessie opnieuw zetten
Staat er een `.env.release` naast `release.ps1`, dan leest het script die in
voordat er iets gebeurt. Handig voor stap 3, die anders elke nieuwe shell
opnieuw met de hand moet:
```
# .env.release — staat in .gitignore, hoort alleen op de release-machine
GITEA_TOKEN=<token met write:repository>
REGISTRY_USER=<gitea-gebruiker>
REGISTRY_TOKEN=<token met write:package>
```
Een variabele die al in je shell staat wint, zodat je per run kunt afwijken
zonder het bestand aan te passen. Het script logt alleen welke namen het heeft
geladen, nooit de waarden.
Twee **verschillende** tokens is hier de bedoeling: de registry heeft
`write:package` nodig en het aanmaken van een release `write:repository`. Eén
breed token voor beide werkt ook, maar dan gaat een repo-schrijfrecht mee bij
elke `docker push`.
> Een **404** in stap 5 betekent bijna altijd dat de token de repo niet mag
> zien: Gitea geeft op repo-endpoints 404 in plaats van 403, om niet te
> verklappen dat iets bestaat. Controleer dan eerst wie de token is en wat hij
> ziet:
>
> ```powershell
> $h = @{ Authorization = "token $env:GITEA_TOKEN" }
> (Invoke-RestMethod https://vibehoogie.duckdns.org/api/v1/user -Headers $h).login
> (Invoke-RestMethod https://vibehoogie.duckdns.org/api/v1/user/repos -Headers $h).full_name
> ```
>
> Staat de repo niet in die lijst, dan mist de token de repository-scope of
> hoort hij bij een account zonder toegang.
---
## Docker
@@ -107,6 +143,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 +341,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
+41
View File
@@ -1,5 +1,46 @@
# Changelog
## [Niet vrijgegeven]
### Gewijzigd
- `release.ps1` leest een `.env.release` naast het script in (niet in git), zodat
`GITEA_TOKEN` en de registry-credentials niet elke shell opnieuw gezet hoeven
te worden; een variabele die al in de omgeving staat wint, en alleen de namen
worden gelogd
- `.gitignore` negeert nu alle `.env.*` op `.env.example` na — `.env.release`
zou anders door een `git add .` in git belanden
- `BEHEER.md` beschrijft welke token-scopes waarvoor nodig zijn
(`write:package` voor de registry, `write:repository` voor een release) en
hoe je een 404 in stap 5 nakijkt
- De draaiende versie komt uit de build in plaats van uit een bestand: de
frontend leest `__APP_VERSION__` via het nieuwe `src/version.js`, niet meer
rechtstreeks uit `frontend/package.json`. `vite.config.js` definieerde die
constante al, maar niets gebruikte hem — een productie-image gebouwd terwijl
er een `-dev` in `package.json` stond, sloeg de update-check stil helemaal
over
## [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
### Gewijzigd
+20 -7
View File
@@ -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);
});
+28
View File
@@ -13,6 +13,7 @@ import { join } from "node:path";
const INDEX_MARKER = "<div id=\"root\"></div>";
const ASSET_BODY = "console.log('vibefinance');";
const FAVICON_BODY = "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
let server;
let basis;
@@ -23,6 +24,7 @@ before(async () => {
mkdirSync(join(staticDir, "assets"));
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, "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", () => {
+2 -1
View File
@@ -13,7 +13,8 @@ export default [
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: { ...globals.browser },
// Door Vite ingebakken tijdens de build (define in vite.config.js)
globals: { ...globals.browser, __APP_VERSION__: "readonly" },
parserOptions: { ecmaFeatures: { jsx: true } },
},
settings: { react: { version: "detect" } },
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vibefinance-frontend",
"version": "0.1.6",
"version": "0.1.8",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -10,7 +10,7 @@ import SchuldTab from "./pages/SchuldTab.jsx";
import VoortgangTab from "./pages/VoortgangTab.jsx";
import { RED, PURPLE_LIGHT } from "./constants/index.js";
import { fmt } from "./utils/format.js";
import { version } from "../package.json";
import { version } from "./version.js";
import { useUpdateCheck } from "./hooks/useUpdateCheck.js";
import ProfielPopup from "./components/ProfielPopup.jsx";
import InstellingenModal from "./components/InstellingenModal.jsx";
+1 -1
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { TABS } from "../constants/index.js";
import { version } from "../../package.json";
import { version } from "../version.js";
import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
import { useApp } from "../context/AppContext.jsx";
+1 -1
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { version } from "../../package.json";
import { version } from "../version.js";
// De check loopt via de eigen backend (/api/update-check): de Gitea API stuurt
// geen CORS-headers, dus de browser mag die niet direct bevragen.
+9
View File
@@ -0,0 +1,9 @@
// Draaiende versie, tijdens de build ingebakken door Vite (`define` in
// vite.config.js). Die leest VITE_APP_VERSION uit de omgeving — de build-arg
// die de Dockerfile en release.ps1 doorgeven — en valt terug op de versie uit
// package.json wanneer die niet gezet is (dev-server, `npm run build` los).
//
// Niet rechtstreeks uit package.json importeren: dan hangt de versie aan een
// bestand in plaats van aan de build, en checkt een image dat gebouwd is met
// een `-dev` in package.json stil nooit meer op updates.
export const version = __APP_VERSION__;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vibefinance",
"version": "0.1.7",
"version": "0.1.9",
"description": "VibeFinance — persoonlijk vermogensbeheer",
"private": true,
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
+29
View File
@@ -28,6 +28,29 @@ param(
$ErrorActionPreference = "Stop"
# ── Geheimen uit .env.release ──────────────────────────────────────────────
# Staat niet in git (.gitignore) en bestaat alleen op de release-machine, zodat
# GITEA_TOKEN niet elke sessie met de hand gezet hoeft te worden. Een variabele
# die al in de omgeving staat wint, zodat je per run kunt afwijken.
$envFile = Join-Path $PSScriptRoot ".env.release"
if (Test-Path $envFile) {
$geladen = @()
foreach ($regel in Get-Content $envFile) {
if ($regel -notmatch '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$') { continue }
$naam = $Matches[1]
$waarde = $Matches[2].Trim().Trim('"').Trim("'")
if (-not $waarde) { continue }
# Al gezet in de shell? Dan die laten staan.
if ([Environment]::GetEnvironmentVariable($naam)) { continue }
[Environment]::SetEnvironmentVariable($naam, $waarde)
$geladen += $naam
}
# Alleen de namen loggen, nooit de waarden.
if ($geladen) {
Write-Host "Geheimen geladen uit .env.release: $($geladen -join ', ')" -ForegroundColor DarkGray
}
}
# ════════════════════════════════════════════════════════════════════════════
# ── PROJECTCONFIGURATIE — pas alleen dit blok aan voor een nieuw project ──
# ════════════════════════════════════════════════════════════════════════════
@@ -53,13 +76,19 @@ if ($appVersion -match "-dev$") {
}
# ── 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"
$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json
if ($frontendPkg.version -ne $appVersion) {
if ($DryRun) {
Write-Host "[DRY-RUN] Versie naar frontend synchroniseren: $($frontendPkg.version) -> $appVersion" -ForegroundColor Yellow
} else {
$frontendPkg.version = $appVersion
[System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10))
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green
}
}
$gitTag = "$appVersion"