# VibeFinance — Beheer De app draait in **2 containers**: `app` (Express API + gebouwde frontend) en `postgres`. Eén image, één origin — geen aparte webserver en geen interne proxy. --- ## Ontwikkeling ```powershell .\dev.ps1 # build + start (standaard) .\dev.ps1 build # alleen bouwen .\dev.ps1 up -Target app # alleen de app herbouwen + herstarten .\dev.ps1 up -Target postgres # alleen de database herstarten .\dev.ps1 logs # logs van de app volgen .\dev.ps1 test # testsuite draaien .\dev.ps1 down # stoppen én volumes wissen .\dev.ps1 push # dev-image naar registry pushen ``` > Op macOS/Linux: gebruik `./dev.sh` met dezelfde argumenten. In dev draait één container beide processen (zie `docker/dev-entrypoint.sh`): | Adres | Wat | |---|---| | http://localhost | Vite dev-server met hot reload | | http://localhost:3001/api/health | de API rechtstreeks | Vite proxyt `/api` naar `localhost:3001` binnen dezelfde container. > ⚠️ Alleen `.\dev.ps1 down` wist de development-database (`docker compose down -v`). > `.\dev.ps1 up` doet een gewone `down` en laat het volume staan. ### Welke versie de dev-app toont De versie in "Over VibeFinance" komt uit `VITE_APP_VERSION`. `dev.ps1` leest die uit de **root** `package.json`, zet hem in de omgeving en synct hem naar `frontend/package.json`. In de dev-container is het een *runtime*-variabele (`docker-compose.dev.yml`, sectie `environment`) en geen build-arg: Vite bakt `__APP_VERSION__` in bij het **starten** van de dev-server, niet bij het bouwen van het image. Is hij leeg, dan valt `vite.config.js` terug op `frontend/package.json`. Ziet de app een oude versie, dan draait de dev-server nog van vóór de bump — `.\dev.ps1 up` hercreëert de container en start Vite opnieuw. Start je de stack handmatig met `docker compose -f docker-compose.dev.yml up`, dan staat `VITE_APP_VERSION` niet in je shell en zie je de versie uit `frontend/package.json`. > Een versie met een suffix (`1.0.1-dev`) onderdrukt de update-melding > permanent — zie [Update-notificaties](#update-notificaties). --- ## Testen ```powershell .\test.ps1 # dependency-check + backend-tests + build + smoketest .\test.ps1 -Unit # dependency-check + backend-tests .\test.ps1 -NoSmoke # zonder de stack te starten .\test.ps1 -Strict # faal als een dependency achterloopt .\test.ps1 -SkipDeps # dependency-check overslaan (bijv. offline) ``` Alles loopt in Docker; Node op de host is niet nodig. | Stap | Wat er gebeurt | |---|---| | Dependency-check | `docker/check-updates.mjs` vergelijkt `package.json` met het npm-register | | Backend-tests | `Dockerfile` target `test` → `node --test` in `backend/test/`, zonder database | | Productiebuild | `Dockerfile` target `production` — faalt de Vite-build, faalt deze stap | | Smoketest | app + postgres starten en `/api/health` en `/` opvragen | De dependency-check draait standaard bij elke testronde, dus je ziet direct of er updates open staan. Pakketten die bewust achterblijven zet je met een reden in `docker/update-exceptions.json`; die worden als `[vast ]` gemeld en laten de check niet falen. Zonder netwerk: `-SkipDeps`. In een CI-gate: `-Strict`. De check los draaien: ```bash docker run --rm -v ${PWD}:/repo -w /repo node:22-alpine node docker/check-updates.mjs docker run --rm -v ${PWD}:/repo -w /repo node:22-alpine node docker/check-updates.mjs --strict ``` Losse backend-tests en linting in een lopende dev-container: ```bash docker compose -f docker-compose.dev.yml exec app npm test docker compose -f docker-compose.dev.yml exec app npm run lint docker compose -f docker-compose.dev.yml exec -w /app/frontend app npm run lint ``` --- ## Tokens > **De `Vibe`-organisatie staat op public.** Alles wat alleen *leest* heeft > daardoor geen token meer nodig: `git clone`, `docker compose pull` en de > update-check van de app werken anoniem. Het host-token hieronder is dus > optioneel geworden. *Schrijven* blijft geauthenticeerd — een publieke > organisatie verandert daar niets aan. Twee tokens, gesplitst op machine en op read/write. De grens die iets betekent ligt tussen werkstation en server, niet tussen push en release: op één machine kan wie het ene token kan lezen, het andere ook. | Token | Machine | Scopes | Gebruikt voor | Nog nodig? | |---|---|---|---|---| | `vibefinance-workstation` | dit werkstation | `write:package`, `write:repository` | `docker push` (`release.ps1`, `dev.ps1 push`) en het aanmaken van de Gitea-release | **Ja** | | `vibefinance-host` | de Docker-host | `read:package`, `read:repository` | `docker compose pull` en de update-check van de app | Nee, zolang de organisatie public is | Aanmaken: Gitea → Settings → Applications → Generate New Token. Doe dat onder een account dat lid is van de `Vibe`-organisatie: voor schrijven volstaat organisatie-lidmaatschap niet automatisch, het account moet ook push-rechten op de repo hebben. **Waar ze terechtkomen** | | Werkstation | Docker-host | |---|---|---| | Registry | `docker login` (credential store), of `REGISTRY_USER` + `REGISTRY_TOKEN` in `.env.release` | niets nodig voor een pull; anders `docker login` als de gebruiker die deployt | | Gitea-API | `GITEA_TOKEN` in `.env.release` | niets nodig; optioneel `UPDATE_CHECK_TOKEN` in `.env` | **Waarom een host-token read-only blijft** — voor het geval de organisatie weer privé gaat en je er alsnog één neerzet. Dat staat in platte tekst in `.env` op een server die de app op poort 3400 publiceert. Met schrijfrechten kan wie bij dat bestand komt — via de container, een shell op die host, of een backup — een eigen image als `latest` naar de registry pushen, en die host pullt `latest` zelf. Read-only houdt je eigen deploy-pad uit de aanvalsketen. ### Wat een foutcode betekent | Status | Betekenis | |---|---| | 401 | Token ongeldig of verlopen. Op de registry: helemaal niet ingelogd. | | 403 | Het token mag de repo wél zien, maar mist de **scope** voor dit endpoint. Gitea noemt de vereiste scope in de response-body. | | 404 | Het token mag de repo niet zien. Gitea geeft geen 403, om niet te verklappen dat iets bestaat: verkeerd account, of geen toegang tot de organisatie. Nu de organisatie public is, wijst een 404 op een leesactie eerder op een verkeerde URL dan op rechten. | Scopes van een bestaand token zijn in Gitea niet aan te passen. Bij een 403 maak je dus een nieuw token aan; bijwerken kan niet. Nakijken wie een 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 ``` Vanuit de app-container op de host valt de body van een fout anders weg — busybox-`wget` gooit die bij een foutstatus weg, en Node zit in het image: ```bash # Anoniem, zoals de app het doet zonder UPDATE_CHECK_TOKEN docker exec vibefinance_app node -e 'fetch(process.env.UPDATE_CHECK_URL,{headers:{Accept:"application/json"}}).then(async r=>console.log(r.status, (await r.text()).slice(0,300)))' # Mét token, precies zoals routes/update.js de header opbouwt docker exec vibefinance_app node -e 'const t=process.env.UPDATE_CHECK_TOKEN; fetch(process.env.UPDATE_CHECK_URL,{headers:{Accept:"application/json",...(t&&{Authorization:`token ${t}`})}}).then(async r=>console.log(r.status, (await r.text()).slice(0,300)))' ``` > Let op: een **ongeldig** token geeft `401`, ook op deze publieke repo — Gitea > valt bij een verkeerd token niet terug op anonieme toegang. Krijg je 401 > terwijl anoniem `200` geeft, haal `UPDATE_CHECK_TOKEN` dan uit `.env` in plaats > van hem te vervangen. Een *leeg* token is onschuldig: `Authorization: token ` > negeert Gitea (`200`), en `config/index.js` zet met `|| null` bij een lege > waarde helemaal geen header. Nagemeten op 2026-09-10: > geen header `200`, `token ongeldig123` `401`, leeg `200`. ### Tokens opruimen De lijst onder Settings → Applications heeft een kolom **Last used**. Zet eerst beide machines om, draai daarna een release en een pull, en kijk een dag later welke tokens niet meer meebewegen — die kun je intrekken. Betrouwbaarder dan uit de naam afleiden wat waar in gebruik is. Trek niets in vóór die controle: het token achter je `docker login` op dit werkstation laat een release op de push falen. --- ## Release workflow ```powershell # 1. Versienummer in package.json van "x.x.x-dev" naar "x.x.x" # 2. Changelog.md: kop van "Niet vrijgegeven" naar "[x.x.x] - datum" # 3. Token staat in .env.release (zie Tokens) — of eenmalig in de shell: $env:GITEA_TOKEN = "" .\release.ps1 # test, bouw (amd64+arm64), push, tag, Gitea-release .\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren .\release.ps1 -NoBuild # sla docker build over, alleen tag + release .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen) # 4. Versienummer in package.json naar "x.x.(x+1)-dev" voor volgende cyclus ``` Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde tag op een Raspberry Pi en op een x86-server werkt. **Wat het script zelf bijwerkt** — met de hand aanpassen is dus dubbel werk: | Bestand | Wat | |---|---| | `frontend/package.json` | versie gelijkgetrokken met de root `package.json` | | `README.md` | de `versie-x.x.x`-badge bovenaan | Beide gebeuren vóór de build, en een `-DryRun` meldt ze zonder te schrijven. Ze blijven na een echte release als wijziging in je working tree staan; neem ze mee in de volgende commit. > **De changelog-kop moet exact `## [x.x.x]` zijn.** `release.ps1` zoekt die kop > om de release-notes te vullen. Staat je werk nog onder `## [Niet vrijgegeven]`, > dan vindt het script niets en valt het terug op de tekst `"Release x.x.x"` — > dan staat er geen inhoudelijke release-notes in Gitea en ziet een gebruiker in > het belletje een placeholder. Dat overkwam 0.1.9. Achteraf repareren kan met > een `PATCH` op `/repos///releases/`, maar vóór de release > omzetten is een stuk minder werk. ### `.env.release` — token niet elke sessie opnieuw zetten Staat er een `.env.release` naast `release.ps1`, dan leest het script die in voordat er iets gebeurt, en hoeft stap 3 niet meer met de hand: ``` # .env.release — staat in .gitignore, hoort alleen op de release-machine GITEA_TOKEN= REGISTRY_USER= REGISTRY_TOKEN= ``` Beide tokenvariabelen krijgen hetzelfde token; zie [Tokens](#tokens) voor waarom dat op deze machine één token mag zijn, en voor wat een 401, 403 of 404 van Gitea betekent. 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. --- ## Docker ```bash # Update via pre-built images docker compose pull && docker compose up -d # Reset inclusief database ⚠️ wist alle data 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: ```bash # 1. Backup maken docker exec vibefinance_postgres pg_dump -U vibefinance vibefinance > backup-$(date +%F).sql # 2. Oude containers en volumes verwijderen docker compose down -v # 3. Nieuwe versie starten docker compose up -d # 4. Backup terugzetten docker exec -i vibefinance_postgres psql -U vibefinance -d vibefinance < backup-YYYY-MM-DD.sql ``` --- ## Architectuur — één app-container De Express-app serveert zowel de API als de gebouwde frontend: | Verzoek | Wat de app doet | |---|---| | `/api/...` | REST API (rate limited) | | `/assets/...` | statische build uit `STATIC_DIR`, 1 jaar `immutable` gecached | | al het overige | `index.html` — fallback voor React-routing | `STATIC_DIR` staat standaard op `/app/public`; daar zet de `Dockerfile` de Vite-build neer. Bestaat die map niet (bijv. in dev), dan serveert de app alleen de API en logt dat bij het starten. > Ontbreekt de build in het image, dan krijg je op `/` een 404 en werkt alleen > `/api/...`. Controleer dan de `frontend-build` stage in de `Dockerfile`. --- ## Logs en diagnose ```bash # Live logs (dev) — beide processen van de app-container docker compose -f docker-compose.dev.yml logs -f app # Live logs (productie) docker compose logs -f # Shell in de app-container (dev) docker compose -f docker-compose.dev.yml exec app sh # Status containers docker compose ps # Database inspecteren (dev) docker exec -it vibefinance-postgres-dev psql -U vibefinance -d vibefinance # Database inspecteren (productie) docker exec -it vibefinance_postgres psql -U vibefinance -d vibefinance ``` --- ## Data persistentie De backend slaat alle data op in **PostgreSQL**. | Omgeving | Volume | Mount pad | |---|---|---| | Productie | `vibefinance_pgdata` | `/var/lib/postgresql` | | Development | `postgres_dev_data` | `/var/lib/postgresql` | > PostgreSQL 18 gebruikt `PGDATA=/var/lib/postgresql/18/docker`. Het volume is gemount op `/var/lib/postgresql` zodat data bewaard blijft na een container restart. **Backup maken:** ```bash # Dev docker exec vibefinance-postgres-dev pg_dump -U vibefinance vibefinance > backup-$(date +%F).sql # Productie docker exec vibefinance_postgres pg_dump -U vibefinance vibefinance > backup-$(date +%F).sql ``` **Backup terugzetten:** ```bash # Dev docker exec -i vibefinance-postgres-dev psql -U vibefinance -d vibefinance < backup-2026-04-08.sql # Productie docker exec -i vibefinance_postgres psql -U vibefinance -d vibefinance < backup-2026-04-08.sql ``` --- ## Gebruikersbeheer De eerste geregistreerde gebruiker krijgt automatisch de **Admin**-rol. Verdere accounts kunnen via de app aangemaakt worden (Gebruikersbeheer, Admin vereist) of via de API. Rollen: | Rol | Rechten | |---|---| | `Admin` | Alles, inclusief gebruikersbeheer | | `Bewerker` | Data lezen en bewerken | | `Viewer` | Alleen lezen | --- ## Productie checklist - [ ] `POSTGRES_PASSWORD` instellen op een sterk wachtwoord - [ ] `JWT_SECRET` aanpassen (genereer met `openssl rand -hex 64`) - [ ] `CORS_ORIGIN` instellen op je domeinnaam - [ ] `APP_PORT` aanpassen indien poort 3400 al in gebruik is - [ ] Testsuite groen (`test.ps1` / `./test.sh`) - [ ] TLS afhandelen via een externe reverse proxy (bijv. Nginx Proxy Manager, Caddy, Traefik) --- ## Dependencies bijwerken Node hoeft niet op de host te staan; de lockfiles zijn met een container bij te werken. Per map (`frontend` en `backend`): ```bash # Laatste versies opzoeken docker run --rm -v ${PWD}/frontend:/w -w /w node:22-alpine npm outdated # Versies in package.json aanpassen, daarna de lockfile herschrijven docker run --rm -v ${PWD}/frontend:/w -w /w node:22-alpine npm install --package-lock-only ``` Daarna altijd de testsuite draaien — die bouwt ook het productie-image, dus een kapotte upgrade valt meteen op: ```powershell .\test.ps1 ``` En de dev-stack herbouwen. `node_modules` staan in anonieme volumes; zonder `--renew-anon-volumes` blijven de oude versies staan. `.\dev.ps1 up` doet dit al automatisch: ```powershell .\dev.ps1 up ``` > Let op bij majors: `eslint-plugin-react` loopt achter op ESLint (peer > `<= ^9.7`), daarom staat de frontend op ESLint 9 en de backend op 10. > Express 5 vereist benoemde wildcards in routes (`/api/*splat`); `server.js` > gebruikt daarom pad-loze middleware in plaats van `app.get("*")`. --- ## Registry ``` vibehoogie.duckdns.org/vibe/vibefinance-app (frontend + backend in één image) Tags: latest productie — meest recente release x.x.x productie — specifieke versie latest-dev development — meest recente dev build x.x.x-dev development — specifieke dev versie ``` Inloggen op de registry: ```bash docker login vibehoogie.duckdns.org ``` Gebruikersnaam is je Gitea-account, wachtwoord het **access token** van deze machine — `vibefinance-workstation`, zie [Tokens](#tokens). Zonder login faalt een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push` controleren dit vóór de build en stoppen dan meteen. Liever niet handmatig inloggen? Zet `REGISTRY_USER` en `REGISTRY_TOKEN` in `.env.release`, dan logt het script zelf in. Twee valkuilen: log in op `vibehoogie.duckdns.org` **zonder** `:3000` (dat is een aparte sleutel in de credential store), en doe het als dezelfde Windows-gebruiker in dezelfde shell — Docker-credentials zijn per gebruikersprofiel. Controleren wat er is opgeslagen: ```powershell docker-credential-desktop list ``` ### Pullen op een deploy-machine De organisatie is public, dus een **pull** werkt anoniem — `docker compose pull && docker compose up -d` en klaar, zonder `docker login`. Controleren zonder te pullen: ```bash docker manifest inspect vibehoogie.duckdns.org/vibe/vibefinance-app:latest ``` Krijg je `401 Unauthorized`, "no basic auth credentials" of "pull access denied", dan is het package niet publiek leesbaar (organisatie weer op privé, of de package-eigenaar wijkt af van de organisatie). Dan alsnog eenmalig op de host: ```bash docker login vibehoogie.duckdns.org # gebruikersnaam: je Gitea-account # wachtwoord: een token met read:package ``` 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`. Moet je terug naar een token, dan kan datzelfde token ook als `UPDATE_CHECK_TOKEN` in de `.env` op deze host — één token voor beide, zie [Tokens](#tokens). --- ## Update-notificaties De app vergelijkt de draaiende versie met de laatste **release** op Gitea en toont dat op het belletje in de topbalk en onder "Over VibeFinance". **Hoe de check loopt** 1. De frontend vraagt `/api/update-check` op de eigen backend — de Gitea API stuurt geen CORS-headers, dus de browser mag die niet direct bevragen. 2. De backend haalt `releases/latest` bij Gitea op en cachet het antwoord 15 minuten. 3. De frontend kijkt bij het openen, daarna elk half uur en bij het terugkeren naar het tabblad. Alleen een écht hoger versienummer geeft een melding. Builds met een `-dev` suffix melden nooit (die lopen voor op de laatste release). Wil je de melding tijdens ontwikkeling toch testen: open de app met `?updatecheck` in de URL. **Configuratie** De bron staat in de root `package.json`: ```json "repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance", "updateCheckUrl": "https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest" ``` `dev.ps1` zet die URL als `UPDATE_CHECK_URL` in de omgeving; in productie komt hij uit `.env` (of de default in `docker-compose.yml`). Beschikbare backend-variabelen: | Variabele | Betekenis | |---|---| | `UPDATE_CHECK_URL` | Release-API die gepolld wordt. Leeg = melding uit. | | `UPDATE_CHECK_TOKEN` | Optioneel. De `Vibe`-organisatie is public, dus `releases/latest` antwoordt anoniem met `200` en de check heeft geen token nodig. Gaat de organisatie weer privé, dan is het host-token nodig (`read:repository`, zie [Tokens](#tokens)): zonder geeft `releases/latest` dan 404, met een verkeerde scope 403 — in beide gevallen blijft de melding leeg, met alleen een `WARN` in het containerlog. | **Een release publiceren** Een kale Git-tag is niet genoeg: `releases/latest` geeft alleen echte releases terug. `release.ps1` maakt die aan (stap 4) met de changelog-sectie van die versie als release-notes. Daarvoor is het werkstation-token met `write:repository` nodig (zie [Tokens](#tokens)), meestal al aanwezig via `.env.release`: ```powershell $env:GITEA_TOKEN = "" # of gewoon uit .env.release .\release.ps1 ``` Zonder token slaat het script die stap over met een waarschuwing — images en tag staan dan wel klaar, maar gebruikers zien geen melding tot de release bestaat.