11 Commits
Author SHA1 Message Date
jhoogie e5f78f5368 Merge pull request 'Versie 1.0.0 in package.json en README-badge, no-store op de API' (#5) from update-1.0.0 into main
Reviewed-on: http://10.0.3.108:3000/Vibe/VibeFinance/pulls/5
2026-09-09 22:05:57 +02:00
Jordy Hoogeveen 09cc6ae488 Versie 1.0.0 in package.json en README-badge, no-store op de API 2026-09-09 21:59:18 +02:00
jhoogie bff64e5012 Merge pull request 'bugfixes and final release' (#4) from update-1.0.0 into main
Reviewed-on: http://10.0.3.108:3000/Vibe/VibeFinance/pulls/4
2026-09-09 17:44:00 +02:00
Jordy Hoogeveen 71cb9b9449 bugfixes and final release 2026-09-09 17:36:34 +02:00
jhoogie 4e4a9ca3e1 Merge pull request 'bugfix update alarm' (#3) from update-0.1.9 into main
Reviewed-on: http://10.0.3.108:3000/Vibe/VibeFinance/pulls/3
2026-09-09 12:48:13 +02:00
Jordy Hoogeveen 51f876efd3 bugfix 2026-09-09 12:43:11 +02:00
jhoogie 00893fa6be Merge pull request 'Update 0.1.8' (#2) from update-0.1.8 into main
Reviewed-on: http://10.0.3.108:3000/Vibe/VibeFinance/pulls/2
2026-09-09 12:17:35 +02:00
jhoogie b5a068d3d8 Merge branch 'main' into update-0.1.8 2026-09-09 12:17:20 +02:00
Jordy Hoogeveen dbdb3ecfc3 Droogloop van release.ps1 laat de working tree ongemoeid 2026-09-09 11:25:35 +02:00
Jordy Hoogeveen 2a4edee192 Favicon-caching en 404 op ontbrekende statische bestanden 2026-09-09 11:04:26 +02:00
jhoogie fdb9771916 Merge pull request 'bugfix test' (#1) from update-0.1.7 into main
Reviewed-on: http://10.0.3.108:3000/Vibe/VibeFinance/pulls/1
2026-09-08 22:27:49 +02:00
18 changed files with 392 additions and 40 deletions
+3
View File
@@ -15,3 +15,6 @@ release.ps1
release.sh release.sh
test.ps1 test.ps1
test.sh test.sh
# Documentatie-afbeeldingen horen niet in het image
docs/
+2 -1
View File
@@ -17,5 +17,6 @@ CORS_ORIGIN=http://localhost:3400
# Update-notificaties (leeg = melding uit) # Update-notificaties (leeg = melding uit)
UPDATE_CHECK_URL=https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest UPDATE_CHECK_URL=https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest
# Alleen nodig als de Gitea-repo niet publiek leesbaar is # Nodig zolang de Gitea-repo niet publiek leesbaar is; scope read:repository.
# Zonder token geeft releases/latest 404 en blijft de update-melding leeg.
# UPDATE_CHECK_TOKEN= # UPDATE_CHECK_TOKEN=
+4
View File
@@ -9,3 +9,7 @@ docker/* text eol=lf
# PowerShell mag CRLF houden (Windows-tooling) # PowerShell mag CRLF houden (Windows-tooling)
*.ps1 text eol=crlf *.ps1 text eol=crlf
# Afbeeldingen nooit door de regeleinde-normalisatie halen
*.png binary
*.jpg binary
+2 -1
View File
@@ -6,7 +6,8 @@ frontend/dist/
# Omgevingsvariabelen (gebruik .env.example als sjabloon) # Omgevingsvariabelen (gebruik .env.example als sjabloon)
.env .env
.env.local .env.*
!.env.example
# Logs en rommel # Logs en rommel
*.log *.log
+145 -16
View File
@@ -74,15 +74,79 @@ docker compose -f docker-compose.dev.yml exec -w /app/frontend app npm run lint
``` ```
--- ---
## Tokens
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 |
|---|---|---|---|
| `vibefinance-workstation` | dit werkstation | `write:package`, `write:repository` | `docker push` (`release.ps1`, `dev.ps1 push`) en het aanmaken van de Gitea-release |
| `vibefinance-host` | de Docker-host | `read:package`, `read:repository` | `docker compose pull` en de update-check van de app |
Aanmaken: Gitea → Settings → Applications → Generate New Token. Doe dat onder een
account dat lid is van de `Vibe`-organisatie — die is privé, dus een token van
een buitenstaander ziet de repo niet.
**Waar ze terechtkomen**
| | Werkstation | Docker-host |
|---|---|---|
| Registry | `docker login` (credential store), of `REGISTRY_USER` + `REGISTRY_TOKEN` in `.env.release` | `docker login` als de gebruiker die deployt |
| Gitea-API | `GITEA_TOKEN` in `.env.release` | `UPDATE_CHECK_TOKEN` in `.env` |
**Waarom het host-token read-only blijft.** 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. |
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
docker exec vibefinance_app node -e 'fetch(process.env.UPDATE_CHECK_URL,{headers:{Accept:"application/json",Authorization:`token ${process.env.UPDATE_CHECK_TOKEN}`}}).then(async r=>console.log(r.status, (await r.text()).slice(0,300)))'
```
### 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 ## Release workflow
```powershell ```powershell
# 1. Versienummer in package.json van "x.x.x-dev" naar "x.x.x" # 1. Versienummer in package.json van "x.x.x-dev" naar "x.x.x"
# 2. Changelog.md bijwerken — die sectie wordt de release-notes # 2. Changelog.md: kop van "Niet vrijgegeven" naar "[x.x.x] - datum"
# 3. Token met release-rechten zetten # 3. Token staat in .env.release (zie Tokens) — of eenmalig in de shell:
$env:GITEA_TOKEN = "<token>" $env:GITEA_TOKEN = "<vibefinance-workstation>"
.\release.ps1 # test, bouw (amd64+arm64), push, tag, Gitea-release .\release.ps1 # test, bouw (amd64+arm64), push, tag, Gitea-release
.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren .\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren
@@ -95,6 +159,45 @@ $env:GITEA_TOKEN = "<token>"
Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde
tag op een Raspberry Pi en op een x86-server werkt. 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/<org>/<repo>/releases/<id>`, 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=<vibefinance-workstation>
REGISTRY_USER=<gitea-gebruiker>
REGISTRY_TOKEN=<vibefinance-workstation>
```
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 ## Docker
@@ -107,6 +210,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:
@@ -280,17 +386,13 @@ Inloggen op de registry:
docker login vibehoogie.duckdns.org docker login vibehoogie.duckdns.org
``` ```
Gebruikersnaam is je Gitea-account, wachtwoord een **access token** met scope Gebruikersnaam is je Gitea-account, wachtwoord het **access token** van deze
`write:package` (Gitea → Settings → Applications → Generate New Token). Zonder machine — `vibefinance-workstation`, zie [Tokens](#tokens). Zonder login faalt
login faalt een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push` een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push` controleren dit
controleren dit vóór de build en stoppen dan meteen. vóór de build en stoppen dan meteen.
Liever niet handmatig inloggen? Zet deze variabelen, dan logt het script zelf in: Liever niet handmatig inloggen? Zet `REGISTRY_USER` en `REGISTRY_TOKEN` in
`.env.release`, dan logt het script zelf in.
```powershell
$env:REGISTRY_USER = "<gitea-gebruiker>"
$env:REGISTRY_TOKEN = "<gitea-token met write:package>"
```
Twee valkuilen: log in op `vibehoogie.duckdns.org` **zonder** `:3000` (dat is een 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 aparte sleutel in de credential store), en doe het als dezelfde Windows-gebruiker
@@ -302,6 +404,32 @@ 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: het token van deze host (vibefinance-host, 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
```
Ditzelfde token gaat als `UPDATE_CHECK_TOKEN` in de `.env` op deze host, voor de
update-check van de app — één token voor beide, zie [Tokens](#tokens).
--- ---
## Update-notificaties ## Update-notificaties
@@ -336,16 +464,17 @@ uit `.env` (of de default in `docker-compose.yml`). Beschikbare backend-variabel
| Variabele | Betekenis | | Variabele | Betekenis |
|---|---| |---|---|
| `UPDATE_CHECK_URL` | Release-API die gepolld wordt. Leeg = melding uit. | | `UPDATE_CHECK_URL` | Release-API die gepolld wordt. Leeg = melding uit. |
| `UPDATE_CHECK_TOKEN` | Alleen nodig als de repo in Gitea niet publiek leesbaar is. | | `UPDATE_CHECK_TOKEN` | Nodig zolang de `Vibe`-organisatie in Gitea privé is: het token van de host (`read:repository`, zie [Tokens](#tokens)). Zonder token geeft `releases/latest` 404, met een verkeerde scope 403 — in beide gevallen blijft de melding leeg, met alleen een `WARN` in het containerlog. |
**Een release publiceren** **Een release publiceren**
Een kale Git-tag is niet genoeg: `releases/latest` geeft alleen echte releases terug. 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.ps1` maakt die aan (stap 4) met de changelog-sectie van die versie als
release-notes. Daarvoor is een Gitea-token met release-rechten nodig: release-notes. Daarvoor is het werkstation-token met `write:repository` nodig
(zie [Tokens](#tokens)), meestal al aanwezig via `.env.release`:
```powershell ```powershell
$env:GITEA_TOKEN = "<token>" $env:GITEA_TOKEN = "<vibefinance-workstation>" # of gewoon uit .env.release
.\release.ps1 .\release.ps1
``` ```
+81
View File
@@ -1,5 +1,86 @@
# Changelog # Changelog
## [Niet vrijgegeven]
### Gewijzigd
- Het label bij een beschikbare update in "Over VibeFinance" is "Nieuwe versie"
(was "Nieuwste versie" in 1.0.0)
### Opgelost
- API-antwoorden sturen nu `Cache-Control: no-store`. Ze hadden alleen een
`ETag` en geen cache-beleid, waardoor de browser zelf mocht bepalen hoe lang
hij ze hergebruikte — per browser en profiel verschillend. Gevolg: de
update-melding bleef in Brave op een oud antwoord hangen (nog van vóór het
werkende token) terwijl een verse Edge het goede kreeg
## [1.0.0] - 2026-09-09
### Gewijzigd
- `BEHEER.md` heeft één centrale `## Tokens`-sectie met het tweetoken-model
(werkstation `write:*`, Docker-host `read:*`) plus de betekenis van een 401,
403 en 404 van Gitea; de Registry-, pull- en update-secties verwijzen daarnaar
in plaats van de scopes elk apart uit te leggen
- `.env.example` noemt bij `UPDATE_CHECK_TOKEN` de vereiste scope
(`read:repository`) en wat je ziet als die ontbreekt
- "Over VibeFinance" noemt de beschikbare update nu "Nieuwste versie" in plaats
van "Nieuw beschikbaar"
- `README.md` bijgewerkt: de Quick Start noemt de `docker login` die een pull uit
de privé-registry vereist (die stap ontbrak), de release-sectie verwijst naar
`.env.release` en `write:repository`, en `UPDATE_CHECK_TOKEN` noemt zijn scope
- `release.ps1` werkt de versiebadge in `README.md` bij, net als de versie in
`frontend/package.json` — die badge stond drie releases achter. Een `-DryRun`
meldt de wijziging en schrijft niet
- `README.md` heeft ruimte voor een screenshot onder de badges
(`docs/screenshot-dashboard.png`); `docs/` staat in `.dockerignore` en `.png`
en `.jpg` zijn in `.gitattributes` als binair vastgelegd
- Badges `React_18` → `React_19` (de app draait op React 19.2)
- `BEHEER.md` legt bij de release-workflow vast wat het script zelf bijwerkt
(`frontend/package.json` en de README-badge), en dat de changelog-kop exact
`## [x.x.x]` moet zijn — anders vallen de release-notes terug op
`"Release x.x.x"`, zoals bij 0.1.9 gebeurde
- Projectboom in `README.md` bevat `docs/` en `frontend/src/version.js`
## [0.1.9] - 2026-09-09
### 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 ## [0.1.7] - 2026-09-08
### Gewijzigd ### Gewijzigd
+18 -5
View File
@@ -6,13 +6,15 @@
Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer
![React](https://img.shields.io/badge/React_18-20232A?style=flat&logo=react&logoColor=61DAFB) ![React](https://img.shields.io/badge/React_19-20232A?style=flat&logo=react&logoColor=61DAFB)
![Node.js](https://img.shields.io/badge/Node.js_22-339933?style=flat&logo=node.js&logoColor=white) ![Node.js](https://img.shields.io/badge/Node.js_22-339933?style=flat&logo=node.js&logoColor=white)
![PostgreSQL](https://img.shields.io/badge/PostgreSQL_18-4169E1?style=flat&logo=postgresql&logoColor=white) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL_18-4169E1?style=flat&logo=postgresql&logoColor=white)
![Docker](https://img.shields.io/badge/Docker_Compose-2496ED?style=flat&logo=docker&logoColor=white) ![Docker](https://img.shields.io/badge/Docker_Compose-2496ED?style=flat&logo=docker&logoColor=white)
![Version](https://img.shields.io/badge/versie-0.1.4-8b5cf6?style=flat) ![Version](https://img.shields.io/badge/versie-1.0.0-8b5cf6?style=flat)
![Built with Claude](https://img.shields.io/badge/Gebouwd_met-Claude.ai-8b5cf6?style=flat&logo=anthropic&logoColor=white) ![Built with Claude](https://img.shields.io/badge/Gebouwd_met-Claude.ai-8b5cf6?style=flat&logo=anthropic&logoColor=white)
<img src="docs/screenshot-dashboard.png" alt="VibeFinance dashboard" width="820">
</div> </div>
--- ---
@@ -46,6 +48,10 @@ openssl rand -hex 64
```bash ```bash
cp .env.example .env # en vul de waarden in cp .env.example .env # en vul de waarden in
# Het app-image staat in een privé-registry, dus eerst inloggen:
docker login vibehoogie.duckdns.org # wachtwoord = Gitea-token met read:package
docker compose pull && docker compose up -d docker compose pull && docker compose up -d
``` ```
@@ -109,7 +115,8 @@ backend/package.json (15 dependencies)
### Productie release ### Productie release
```powershell ```powershell
$env:GITEA_TOKEN = "<token met release-rechten>" # token: staat in .env.release, of eenmalig in de shell
$env:GITEA_TOKEN = "<token met write:repository>"
.\release.ps1 .\release.ps1
``` ```
@@ -147,11 +154,15 @@ vibefinance/
├── docker/ ├── docker/
│ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container │ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container
│ │
├── docs/
│ └── screenshot-dashboard.png # Screenshot bovenaan deze README
│
├── frontend/ # React / Vite SPA (build → dist) ├── frontend/ # React / Vite SPA (build → dist)
│ ├── vite.config.js # /api proxy naar de API in dezelfde container │ ├── vite.config.js # /api proxy naar de API in dezelfde container
│ └── src/ │ └── src/
│ ├── App.jsx # Root – wiring + idle-logout │ ├── App.jsx # Root – wiring + idle-logout
│ ├── main.jsx │ ├── main.jsx
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
│ ├── constants/ # Kleuren, tabs, begindata │ ├── constants/ # Kleuren, tabs, begindata
│ ├── utils/ # Formatters, helpers │ ├── utils/ # Formatters, helpers
│ ├── hooks/ # useTheme, useUpdateCheck │ ├── hooks/ # useTheme, useUpdateCheck
@@ -248,7 +259,7 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
| `PORT` | `3001` | Poort binnen de container | | `PORT` | `3001` | Poort binnen de container |
| `STATIC_DIR` | `/app/public` | Map met de frontend-build | | `STATIC_DIR` | `/app/public` | Map met de frontend-build |
| `UPDATE_CHECK_URL` | Gitea releases-API | Bron update-melding (leeg = uit) | | `UPDATE_CHECK_URL` | Gitea releases-API | Bron update-melding (leeg = uit) |
| `UPDATE_CHECK_TOKEN` | — | Alleen voor een niet-publieke repo | | `UPDATE_CHECK_TOKEN` | — | Nodig zolang de Gitea-repo privé is; scope `read:repository` |
--- ---
@@ -277,7 +288,9 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
## 🛠️ Beheer ## 🛠️ Beheer
Zie `BEHEER.md` voor commando's voor lokale ontwikkeling, releases, logs en database-toegang. Zie `BEHEER.md` voor commando's voor lokale ontwikkeling, releases, logs en
database-toegang — en voor de sectie **Tokens**: welk Gitea-token op welke
machine hoort, met welke scopes, en wat een 401, 403 of 404 van Gitea betekent.
--- ---
+31 -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";
@@ -56,6 +56,17 @@ if (config.NODE_ENV !== "test") {
app.use(pinoHttp({ logger })); app.use(pinoHttp({ logger }));
} }
// ── Geen browsercache op de API ───────────────────────────────────────────────
// Zonder Cache-Control mag de browser zelf bepalen hoe lang hij een antwoord
// hergebruikt (heuristische caching). Dat verschilt per browser en profiel: de
// update-melding bleef in de ene browser op een oud antwoord hangen terwijl een
// verse browser het goede kreeg. API-antwoorden zijn per gebruiker en per moment,
// dus die horen nooit uit de cache te komen.
app.use("/api", (_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
// ── API-routes ──────────────────────────────────────────────────────────────── // ── API-routes ────────────────────────────────────────────────────────────────
app.use("/api/health", healthRouter); app.use("/api/health", healthRouter);
app.use("/api/update-check", updateRouter); app.use("/api/update-check", updateRouter);
@@ -73,21 +84,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);
}); });
+37
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;
@@ -66,6 +68,15 @@ describe("API", () => {
assert.equal(body.latest, null); assert.equal(body.latest, null);
assert.equal(body.disabled, true); assert.equal(body.disabled, true);
}); });
// Zonder expliciete header bepaalt de browser zelf hoe lang hij een
// API-antwoord hergebruikt, en dat verschilt per browser.
it("API-antwoorden komen nooit uit de browsercache", async () => {
for (const pad of ["/api/health", "/api/update-check", "/api/bestaat-niet"]) {
const res = await fetch(`${basis}${pad}`);
assert.match(res.headers.get("cache-control") ?? "", /no-store/, pad);
}
});
}); });
describe("Frontend serveren", () => { describe("Frontend serveren", () => {
@@ -93,6 +104,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", () => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

+2 -1
View File
@@ -13,7 +13,8 @@ export default [
languageOptions: { languageOptions: {
ecmaVersion: "latest", ecmaVersion: "latest",
sourceType: "module", 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 } }, parserOptions: { ecmaFeatures: { jsx: true } },
}, },
settings: { react: { version: "detect" } }, settings: { react: { version: "detect" } },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "vibefinance-frontend", "name": "vibefinance-frontend",
"version": "0.1.6", "version": "1.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -10,7 +10,7 @@ import SchuldTab from "./pages/SchuldTab.jsx";
import VoortgangTab from "./pages/VoortgangTab.jsx"; import VoortgangTab from "./pages/VoortgangTab.jsx";
import { RED, PURPLE_LIGHT } from "./constants/index.js"; import { RED, PURPLE_LIGHT } from "./constants/index.js";
import { fmt } from "./utils/format.js"; import { fmt } from "./utils/format.js";
import { version } from "../package.json"; import { version } from "./version.js";
import { useUpdateCheck } from "./hooks/useUpdateCheck.js"; import { useUpdateCheck } from "./hooks/useUpdateCheck.js";
import ProfielPopup from "./components/ProfielPopup.jsx"; import ProfielPopup from "./components/ProfielPopup.jsx";
import InstellingenModal from "./components/InstellingenModal.jsx"; import InstellingenModal from "./components/InstellingenModal.jsx";
+2 -2
View File
@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { TABS } from "../constants/index.js"; import { TABS } from "../constants/index.js";
import { version } from "../../package.json"; import { version } from "../version.js";
import { useUpdateCheck } from "../hooks/useUpdateCheck.js"; import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
import { useApp } from "../context/AppContext.jsx"; import { useApp } from "../context/AppContext.jsx";
@@ -165,7 +165,7 @@ function OverPopup({ onClose, nieuweVersie, T }) {
{nieuweVersie && ( {nieuweVersie && (
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
<span style={{ fontSize: 13, color: T.muted }}>Nieuw beschikbaar</span> <span style={{ fontSize: 13, color: T.muted }}>Nieuwe versie</span>
<span style={{ fontSize: 12, fontWeight: 700, color: "#f97316", background: "rgba(249,115,22,0.12)", borderRadius: 6, padding: "3px 10px" }}> <span style={{ fontSize: 12, fontWeight: 700, color: "#f97316", background: "rgba(249,115,22,0.12)", borderRadius: 6, padding: "3px 10px" }}>
v{nieuweVersie.latest} v{nieuweVersie.latest}
</span> </span>
+1 -1
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; 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 // 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. // 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", "name": "vibefinance",
"version": "0.1.7", "version": "1.0.0",
"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",
+49
View File
@@ -28,6 +28,29 @@ param(
$ErrorActionPreference = "Stop" $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 ── # ── PROJECTCONFIGURATIE — pas alleen dit blok aan voor een nieuw project ──
# ════════════════════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════════════════════
@@ -53,13 +76,39 @@ 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
} }
}
# ── Versiebadge in README.md ──────────────────────────────────────────────
# Anders loopt die badge stil achter: hij stond drie releases achter voordat
# dit erin zat, en het script kent het nummer toch al. Zelfde regel als
# hierboven: een droogloop meldt het, maar schrijft niet.
$readmePath = Join-Path $PSScriptRoot "README.md"
if (Test-Path $readmePath) {
$readme = [System.IO.File]::ReadAllText($readmePath)
$patroon = "badge/versie-[^-]+-"
$nieuw = "badge/versie-$appVersion-"
$huidig = [regex]::Match($readme, $patroon)
if ($huidig.Success -and $huidig.Value -ne $nieuw) {
if ($DryRun) {
Write-Host "[DRY-RUN] Versiebadge in README.md: $($huidig.Value) -> $nieuw" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($readmePath, ($readme -replace $patroon, $nieuw))
Write-Host "Versiebadge in README.md bijgewerkt: $appVersion" -ForegroundColor Green
}
}
}
$gitTag = "$appVersion" $gitTag = "$appVersion"