21 Commits
Author SHA1 Message Date
Jordy Hoogeveen e315039c35 bugfix 2026-09-11 14:05:55 +02:00
Jordy Hoogeveen 0d41c0af0b bugfix 2026-09-10 21:33:44 +02:00
Jordy Hoogeveen 04e70208bd bugfix 2026-09-10 16:51:06 +02:00
Jordy Hoogeveen ceff734793 bugfix 2026-09-10 16:24:30 +02:00
Jordy Hoogeveen 624f51679a bugfix 2026-09-10 15:40:27 +02:00
jhoogie 5f90331694 Merge pull request 'Versie 1.0.1 - versleepbare kolommen en een enkel Gitea-token' (#6) from update-1.0.1 into main 2026-09-10 15:01:55 +02:00
Jordy HoogeveenandClaude Opus 5 4e923fa998 Versienummer naar 1.0.2-dev voor de volgende cyclus
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 14:44:24 +02:00
Jordy HoogeveenandClaude Opus 5 857322a490 Tokendocumentatie terug naar één werkstation-token
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 14:30:18 +02:00
Jordy HoogeveenandClaude Opus 5 6fd4f4766b Versie 1.0.1 in README-badge en frontend, changelog vrijgegeven
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 14:16:09 +02:00
Jordy Hoogeveen 1b14553417 bugfixes 2026-09-10 14:03:34 +02:00
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
26 changed files with 1108 additions and 117 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/
+5 -1
View File
@@ -17,5 +17,9 @@ 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 # De Gitea-organisatie is publiek, dus de check werkt anoniem — laat dit staan.
# Alleen nodig als de organisatie weer privé wordt; scope read:repository.
# Dan geeft releases/latest zonder token 404 en blijft de melding leeg.
# Let op: een ongeldig token geeft 401, ook op een publieke repo. Bij twijfel
# de regel uitgecommentarieerd laten in plaats van een token te gokken.
# 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
+314 -26
View File
@@ -32,6 +32,25 @@ Vite proxyt `/api` naar `localhost:3001` binnen dezelfde container.
> ⚠️ Alleen `.\dev.ps1 down` wist de development-database (`docker compose down -v`). > ⚠️ Alleen `.\dev.ps1 down` wist de development-database (`docker compose down -v`).
> `.\dev.ps1 up` doet een gewone `down` en laat het volume staan. > `.\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 ## Testen
@@ -74,27 +93,200 @@ docker compose -f docker-compose.dev.yml exec -w /app/frontend app npm run lint
``` ```
--- ---
## Tokens
**Eén token, op het werkstation.** Dat is genoeg omdat de `Vibe`-organisatie op
public staat: alles wat alleen *leest* heeft geen token meer nodig — `git clone`,
`docker compose pull` en de update-check van de app werken anoniem. Wat overblijft
is schrijven, en dat gebeurt allemaal vanaf deze ene machine.
| Token | Machine | Scopes | Gebruikt voor |
|---|---|---|---|
| `vibefinance-workstation` | dit werkstation | `write:package`, `write:repository` | `docker push` (`release.ps1`, `dev.ps1 push`) én het aanmaken van de Gitea-release |
Er is geen apart host-token en geen apart registry-token:
- **`git push`** gaat over SSH (`gitea@…:Vibe/VibeFinance.git`), dus die gebruikt
je SSH-sleutel en niet dit token. Werkt SSH even niet — bijvoorbeeld omdat de
ssh-agent niet meedoet in een niet-interactieve shell, wat `Permission denied
(publickey,password)` geeft — dan kun je *lezen* nu anoniem over https, omdat
de organisatie public is:
```powershell
git fetch https://vibehoogie.duckdns.org/Vibe/VibeFinance.git --prune --tags "+refs/heads/*:refs/remotes/origin/*"
```
Die refspec werkt ook `origin/*` bij, dus daarna doen `git merge --ff-only
origin/main` en `git log origin/main` het weer. Pushen blijft SSH; `release.ps1`
leest daarvoor `core.sshCommand` uit, omdat op Windows alleen de OpenSSH van het
systeem met de ssh-agent praat en Git's eigen `ssh` niet.
- **De registry** heeft geen eigen variabele nodig: `release.ps1` en `dev.ps1`
vallen bij een ontbrekende `REGISTRY_TOKEN` terug op `GITEA_TOKEN`. Zet alleen
`REGISTRY_USER` erbij, zodat het script zelf kan inloggen als de credential
store leeg is.
- **De Docker-host** hoeft niets meer te hebben: pull en update-check zijn anoniem.
Aanmaken: Gitea → Settings → Applications → Generate New Token, met **beide**
scopes aangevinkt. Doe dat onder een account dat push-rechten op de repo heeft —
lid zijn van de organisatie is daarvoor niet automatisch genoeg. Scopes van een
bestaand token zijn later niet aan te passen, dus vink ze in één keer goed aan.
**Waar hij terechtkomt** — `.env.release` naast de scripts, niet in git
(`.gitignore` pakt `.env.*`):
```ini
GITEA_TOKEN=<token met write:package + write:repository>
REGISTRY_USER=<je Gitea-account>
```
Een variabele die al in je shell staat wint, zodat je per run kunt afwijken. Een
lege waarde in het bestand wordt overgeslagen.
**Als de organisatie weer privé gaat** heb je er wél een tweede bij nodig: een
read-only token op de Docker-host, voor `docker compose pull` en
`UPDATE_CHECK_TOKEN`. Houd dat token dan read-only. Het 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, verlopen of ingetrokken — Gitea herkent hem niet. Op de registry: helemaal niet ingelogd. |
| 403 | Het token **bestaat en werkt**, 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.
**Leeft mijn token nog?** Juist dat onderscheid tussen 401 en 403 is de test.
Vraag *niet* `/api/v1/user` op om te kijken "wie het token is": het
werkstation-token heeft geen `read:user`, dus dat endpoint geeft altijd 403 en
zegt niets over geldigheid. Kijk naar de statuscode, niet naar de inhoud:
```powershell
$u = "https://vibehoogie.duckdns.org/api/v1/user/repos"
try { (Invoke-WebRequest $u -Headers @{ Authorization = "token $env:GITEA_TOKEN" } -UseBasicParsing).StatusCode }
catch { $_.Exception.Response.StatusCode.value__ }
```
`403` = het token leeft (alleen deze scope ontbreekt). `401` = ingetrokken of
fout gekopieerd. Nagemeten op 2026-09-10: geen header `401`, een nep-token van 40
nullen `401`, het werkstation-token `403`.
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`.
### Oude tokens opruimen
Nu er één token overblijft, kunnen de rest weg — maar niet op de gok. De lijst
onder Settings → Applications heeft een kolom **Last used**. Werkwijze:
1. Zet het nieuwe token in `.env.release` en draai een release die helemaal
doorloopt (inclusief `docker push` en stap 5).
2. Kijk in de lijst welk token net **Last used** heeft gekregen.
3. Wacht een dag en trek de tokens in die niet meer meebewegen.
Betrouwbaarder dan uit de naam afleiden wat waar in gebruik is. Trek vooral niets
in vóór die controle: het token achter je `docker login` zit in de credential
store en niet in een bestand, dus je ziet aan niets welke het is — trek je die
in, dan valt de push in stap 3 om. Loopt dat toch mis, dan is
`docker login vibehoogie.duckdns.org` met het nieuwe token genoeg.
--- ---
## 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. Wijzigingen in Changelog.md onder de kop "## [Niet vrijgegeven]"
# 3. Token met release-rechten zetten # (de kop zelf omzetten hoeft niet — dat doet het script)
$env:GITEA_TOKEN = "<token>" # 3. Token staat in .env.release (zie Tokens) — of eenmalig in de shell:
$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
.\release.ps1 -NoBuild # sla docker build over, alleen tag + release .\release.ps1 -NoBuild # sla docker build over, alleen tag + release
.\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen) .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
.\release.ps1 -NoChangelog # publiceer bewust zonder release-notes
# 4. Versienummer in package.json naar "x.x.(x+1)-dev" voor volgende cyclus # Klaar. Het versienummer voor de volgende cyclus zet het script zelf.
``` ```
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 | Wanneer |
|---|---|---|
| `Changelog.md` | kop `## [Niet vrijgegeven]` → `## [x.x.x] - datum` | vóór de build |
| Git-tag | `vx.x.x` op `HEAD`, gepusht naar origin | stap 4 |
| `frontend/package.json` | versie gelijkgetrokken met de root `package.json` | vóór de build |
| `README.md` | de `versie-x.x.x`-badge bovenaan | vóór de build |
| `package.json` (root) | versie → `x.x.(x+1)-dev` voor de volgende cyclus | **ná** het publiceren |
Een `-DryRun` meldt alle vier zonder te schrijven. Ze blijven na een echte
release als wijziging in je working tree staan; neem ze mee in de volgende
commit.
De laatste staat bewust ná het publiceren: breekt de release eerder af, dan
schuift het versienummer niet vooruit. Bijeffect dat je moet kennen — na een
geslaagde release staat er een `-dev` nummer in `package.json`, en de controle
bovenin het script weigert een `-dev` versie. Dat sluit een dubbele publicatie
uit, maar wil je dezelfde versie bewust opnieuw releasen (bijvoorbeeld omdat een
tag is verdwenen), zet het nummer dan eerst zelf terug naar `x.x.x`.
> **Zonder changelog-sectie breekt het script af.** De release-notes komen uit de
> sectie `## [x.x.x]` in `Changelog.md`. Vond het script daar niets, dan
> publiceerde het stil de tekst `"Release x.x.x"` — dat gebeurde bij 0.1.9 en
> 1.0.2. Nu zet het de kop `## [Niet vrijgegeven]` zelf om, en als er ook dán
> niets over deze versie in staat, stopt het vóór de build met de melding wat je
> moet toevoegen. Wil je bewust zonder notes publiceren, gebruik `-NoChangelog`.
>
> De controle staat vóór de drie auto-updates hierboven, zodat een afgebroken
> release geen gewijzigde bestanden achterlaat. Is een release toch al gepubliceerd
> met de placeholder, dan repareer je dat met een `PATCH` op
> `/repos/<org>/<repo>/releases/<id>` met de juiste `body`.
### `.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:
```ini
# .env.release — staat in .gitignore, hoort alleen op de release-machine
GITEA_TOKEN=<vibefinance-workstation>
REGISTRY_USER=<gitea-gebruiker>
```
`REGISTRY_TOKEN` hoeft er niet in: beide scripts vallen bij een ontbrekende
`REGISTRY_TOKEN` terug op `GITEA_TOKEN`. Zie [Tokens](#tokens) voor de scopes 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 +299,14 @@ docker compose pull && docker compose up -d
docker compose down -v && docker compose up -d docker compose down -v && docker compose up -d
``` ```
Draai dit in de map met `docker-compose.yml` en `.env`. `docker compose` zoekt
dat bestand namelijk in de huidige map en kijkt niet omhoog; staat het er niet,
dan is het antwoord `no configuration file provided: not found`. Met
`docker compose -f /pad/naar/docker-compose.yml` kun je het expliciet meegeven.
Pullen zelf werkt anoniem zolang de organisatie public is — 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:
@@ -133,9 +333,11 @@ De Express-app serveert zowel de API als de gebouwde frontend:
| Verzoek | Wat de app doet | | Verzoek | Wat de app doet |
|---|---| |---|---|
| `/api/...` | REST API (rate limited) | | `/api/...` | REST API (rate limited, `Cache-Control: no-store`) |
| `/assets/...` | statische build uit `STATIC_DIR`, 1 jaar `immutable` gecached | | `/assets/...` | gehashte build-bestanden uit `STATIC_DIR`, 1 jaar `immutable` |
| al het overige | `index.html` — fallback voor React-routing | | `/favicon.svg`, `/icons/...` | vaste namen, dus `max-age=0, must-revalidate` |
| pad mét extensie zonder bestand | echte 404, niet `index.html` |
| al het overige | `index.html` — fallback voor React-routing, `no-store` |
`STATIC_DIR` staat standaard op `/app/public`; daar zet de `Dockerfile` de `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 Vite-build neer. Bestaat die map niet (bijv. in dev), dan serveert de app
@@ -181,6 +383,12 @@ De backend slaat alle data op in **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. > 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.
Daarnaast heeft de app zelf een backup onder **Instellingen**: die schrijft een
ZIP met de data van de *ingelogde* gebruiker (`data.json` + `meta.json`) en leest
zo'n ZIP ook weer in. Handig per gebruiker, maar het is geen servercopie — accounts,
wachtwoordhashes en de data van andere gebruikers zitten er niet in. Voor een
volledige backup gebruik je `pg_dump` hieronder.
**Backup maken:** **Backup maken:**
```bash ```bash
@@ -207,12 +415,25 @@ docker exec -i vibefinance_postgres psql -U vibefinance -d vibefinance < backup-
De eerste geregistreerde gebruiker krijgt automatisch de **Admin**-rol. Verdere accounts kunnen via de app aangemaakt worden (Gebruikersbeheer, Admin vereist) of via de API. 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: ### Rollen
| Rol | Rechten |
De rolkeuze in Gebruikersbeheer biedt drie waarden, maar de backend dwingt er op
dit moment één af:
| Rol | Wat de code ermee doet |
|---|---| |---|---|
| `Admin` | Alles, inclusief gebruikersbeheer | | `Admin` | Enige rol die wordt gecontroleerd: `requireAdmin` in `middleware/auth.js` bewaakt alle routes onder `/api/users` |
| `Bewerker` | Data lezen en bewerken | | `Bewerker` | Geen eigen rechten — gelijk aan `Viewer` |
| `Viewer` | Alleen lezen | | `Viewer` | Standaardrol voor een nieuw account; mag de eigen data lezen én schrijven |
Dat `Viewer` toch mag schrijven is geen gat in de afscherming: `/api/data` werkt
per gebruiker en gaat altijd over de rij van het eigen token (`req.user.sub`).
Niemand ziet dus data van een ander. Wat ontbreekt is een read-only account —
wil je dat, dan is er een controle op `rol` nodig bij `PUT`/`DELETE /api/data`.
> De "Gebruikers"-knop in de navigatiebalk staat bij iedereen in beeld; bij een
> niet-Admin antwoordt `/api/users` met `403` en blijft de lijst leeg. De
> afscherming zit dus in de API, niet in de navigatie.
--- ---
@@ -274,23 +495,24 @@ Tags:
x.x.x-dev development — specifieke dev versie x.x.x-dev development — specifieke dev versie
``` ```
> Image-tags hebben géén `v`-prefix (`1.0.4`), Git-tags wél (`v1.0.4`). Dat is
> geen inconsistentie maar de conventie aan beide kanten: container-registries
> taggen op het kale nummer, Git-releases met `v`.
Inloggen op de registry: Inloggen op de registry:
```bash ```bash
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` in `.env.release`, dan logt
het script zelf in met `GITEA_TOKEN` — een aparte `REGISTRY_TOKEN` is alleen
```powershell nodig als je daar bewust een ánder token voor wilt gebruiken.
$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 +524,35 @@ Controleren wat er is opgeslagen:
docker-credential-desktop list 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 ## Update-notificaties
@@ -319,7 +570,43 @@ dat op het belletje in de topbalk en onder "Over VibeFinance".
Builds met een `-dev` suffix melden nooit (die lopen voor op de laatste release). 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` Wil je de melding tijdens ontwikkeling toch testen: open de app met `?updatecheck`
in de URL. in de URL. Let op dat `isNieuwer` alleen bij een écht hoger nummer aanslaat — bij
`1.0.2-dev` naast release `1.0.1` blijft de kaart dus ook met `?updatecheck` leeg.
Zet dan tijdelijk een lagere versie in de root `package.json`.
**Wat de melding toont**
Alleen de kop, de versieregel en een link:
```
🔔 Nieuwe versie beschikbaar
v1.0.1 is uitgebracht. Je gebruikt momenteel v1.0.0.
Release bekijken →
```
De release-notes staan er bewust **niet** in. `/api/update-check` stuurt ze wel
mee als `notes`, maar dat is de ruwe Markdown uit de changelog; in een kaart van
280px leverde dat zichtbare `### `-koppen en afgekapte regels op. Wie de details
wil, volgt de link. Het veld blijft in de API zitten voor het geval je ze ooit
ergens met meer ruimte wilt tonen — dan is Markdown renderen de volgende stap.
**De release-link hangt aan Gitea's `ROOT_URL`**
De `url` in het antwoord is de `html_url` van de release, en die stelt Gitea bij
elk verzoek samen uit `ROOT_URL` in `app.ini` — hij wordt *niet* per release
opgeslagen. Staat `ROOT_URL` op een intern adres, dan wijst "Release bekijken"
naar iets dat buiten je netwerk niet bestaat, terwijl de melding zelf gewoon
werkt (die leest alleen het versienummer).
Nakijken welke waarde Gitea daadwerkelijk gebruikt — de `issuer` is `ROOT_URL`:
```bash
curl -s https://vibehoogie.duckdns.org/.well-known/openid-configuration | grep issuer
```
`app.ini` wordt alleen bij het opstarten gelezen, dus na een wijziging moet Gitea
herstarten. Omdat de URL wordt samengesteld en niet bewaard, zijn daarna álle
releases in één keer goed — ook de oude. Een `PATCH` per release is niet nodig.
**Configuratie** **Configuratie**
@@ -336,16 +623,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` | 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 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
``` ```
+214
View File
@@ -1,5 +1,219 @@
# Changelog # Changelog
## [Niet vrijgegeven]
### Gewijzigd
- De documentatie is gelijkgetrokken met de code. `README.md`: de bestandsboom
miste `backend/src/logger.js`, `docker/check-updates.mjs`,
`frontend/public/`, `InstellingenModal.jsx`, `LoginBackground.jsx` en de
losse bestanden in `utils/`, en noemde de update-route `update-check` terwijl
het bestand `routes/update.js` heet. De schermentabel gebruikt nu de labels
zoals ze in `constants/index.js` staan ("Schuld", "Voortgang Schuld & EV") en
vermeldt **Instellingen** — thema plus backup/restore via ZIP, dat nergens in
de README stond
- De tabel "wat de app met een verzoek doet" (in beide bestanden) noemde alleen
`/assets/...` en de SPA-fallback. `server.js` kent vier gevallen: `no-store`
op de API en op HTML, een jaar `immutable` op de gehashte bundels,
`must-revalidate` op bestanden met een vaste naam (`favicon.svg`, `icons/*`)
en een echte 404 op een pad mét extensie waar geen bestand achter zit
- `BEHEER.md` beschreef `Bewerker` als "data lezen en bewerken" en `Viewer` als
"alleen lezen". Zo werkt het niet: `requireAdmin` is de enige rolcontrole in
de code en bewaakt alleen `/api/users`. Een `Viewer` mag daardoor gewoon zijn
eigen data schrijven — geen gat in de afscherming, want `/api/data` gaat altijd
over de rij van het eigen token, maar wél iets anders dan er stond. De sectie
zegt nu wat de code doet en wat er nodig is voor een echt read-only account
- `README.md` en `BEHEER.md` wijzen bij backup naar het juiste niveau: de ZIP
onder Instellingen bevat alleen de data van de ingelogde gebruiker, `pg_dump`
is de volledige servercopie
- `DATABASE_URL` staat in de variabelentabel van `README.md`. Hij wordt door
`docker-compose.yml` samengesteld uit de `POSTGRES_*` waarden, dus zelf zetten
is alleen nodig buiten Compose
## [1.0.4] - 2026-09-10
### Gewijzigd
- Git-tags krijgen weer een `v`-prefix (`v1.0.4`), net als t/m `v1.0.0`. Sinds
1.0.1 was dat `1.0.1`, `1.0.2`, `1.0.3` — en omdat Gitea de tag op de
release-pagina toont, stond daar het kale nummer terwijl de release-naam wél
`v1.0.3` was. De image-tags in de registry houden bewust géén `v`; dat is de
conventie voor container-tags. De update-check verdraagt beide: backend en
frontend strippen een leidende `v` voordat ze versies vergelijken
### Opgelost
- Het omzetten van de changelog-kop slokte de lege regel eronder op, waardoor
`## [x.x.x] - datum` direct tegen `### Toegevoegd` aan kwam te staan. Oorzaak
was `\s*$` in het zoekpatroon: `\s` matcht ook newlines. Het patroon eindigt nu
op `[ \t]*(?=\r?$)` — een lookahead, want dit bestand heeft CRLF en met alleen
`[ \t]*$` matchte de kop helemaal niet meer (de `\r` zit ertussen). Zo blijft
de match binnen één regel én blijft het regeleinde ongemoeid
## [1.0.3] - 2026-09-10
### Toegevoegd
- `release.ps1` zet de changelog-kop `## [Niet vrijgegeven]` nu zelf om naar
`## [x.x.x] - datum`, net zoals het al deed met `frontend/package.json` en de
README-badge. Handmatig omzetten hoeft dus niet meer
- Staat er ook na die omzetting niets over deze versie in `Changelog.md`, dan
breekt het script af **vóór** de build met de melding wat je moet toevoegen.
Eerder viel het stil terug op de tekst `"Release x.x.x"` en meldde het
"Gepubliceerd" — dat overkwam 0.1.9 en 1.0.2, en is achteraf alleen met een
`PATCH` op de release te herstellen. Nieuwe switch `-NoChangelog` om bewust
zonder release-notes te publiceren
- `release.ps1` zet na een geslaagde release de versie in de root
`package.json` op `x.x.(x+1)-dev`. Dat was voorheen de laatste handmatige stap
van de release-workflow. Het staat bewust ná het publiceren, zodat een
afgebroken release het nummer niet vooruit schuift
### Opgelost
- **`-DryRun` in combinatie met `-Force` verwijderde écht de Git-tag.** De twee
commando's in stap 4 (`git tag -d` en `git push origin :refs/tags/x.x.x`)
liepen niet via `Invoke-Cmd` en hadden dus geen droogloop-controle. Gitea
ruimt de release op zodra de tag verdwijnt, dus een droogloop kon een
gepubliceerde release wissen — zo is de release van 1.0.2 verloren gegaan en
opnieuw aangemaakt. Onder `-DryRun` worden beide nu alleen gemeld
### Gewijzigd
- De changelog-controle staat vóór de drie auto-updates, zodat een afgebroken
release geen gewijzigde bestanden achterlaat terwijl hij "er is niets
gepubliceerd" meldt
- Stap 5 leest de release-notes uit het geheugen in plaats van opnieuw van
schijf. Een droogloop schrijft de kop niet, en toonde daardoor de placeholder
in de payload — precies wat je met een droogloop wilt uitsluiten
- De Quick Start in `README.md` begint nu met het ophalen van
`docker-compose.yml` (clone, of alleen die twee bestanden via `raw`). Zonder
dat bestand antwoordt `docker compose pull` met `no configuration file
provided: not found`, en die stap ontbrak. Beide foutmeldingen staan er nu bij
met hun oorzaak
## [1.0.2] - 2026-09-10
### Gewijzigd
- De update-melding op het belletje toont alleen nog de kop, de versieregel en de
link naar de release. De release-notes stonden er als ruwe Markdown in — in een
kaart van 280px leverde dat zichtbare `### `-koppen en afgekapte regels op.
`/api/update-check` stuurt `notes` nog wel mee, zodat ze later ergens met meer
ruimte gebruikt kunnen worden
- `BEHEER.md` legt vast dat de release-link uit Gitea's `ROOT_URL` wordt
samengesteld en niet per release wordt opgeslagen: na een `ROOT_URL`-wijziging
plus herstart zijn álle releases in één keer goed, ook de oude. Verder staat er
nu hoe je aan het verschil tussen `401` en `403` ziet of een token nog leeft, en
hoe je anoniem over https fetcht als SSH niet meedoet
## [1.0.1] - 2026-09-10
### Toegevoegd
- Kolommen in de tabellen op Eigen Vermogen ("Geïnvesteerde Fiat") en Voortgang
zijn versleepbaar: pak de rechterrand van een kolomkop. De breedtes gaan per
tabel naar localStorage, dus ze overleven een tabwissel en een herlaad;
dubbelklik op de greep zet één kolom terug op zijn standaardbreedte. De logica
staat in de nieuwe hook `frontend/src/hooks/useKolomBreedtes.js`, de greep zelf
is `.kolom-grip` in `App.css`
### Gewijzigd
- De `Vibe`-organisatie in Gitea staat op **public**, dus lezen vereist geen
token meer: `git clone`, `docker compose pull` en de update-check werken
anoniem. `UPDATE_CHECK_TOKEN` in `.env` is daarmee optioneel — de code
ondersteunt hem nog als vangnet voor het geval de organisatie weer privé
wordt. Schrijven verandert niet: `docker push` en het aanmaken van een release
hebben nog steeds het werkstation-token nodig
- Het tweetoken-model in `BEHEER.md` is teruggebracht tot **één** Gitea-token op
het werkstation, met `write:package` en `write:repository`. Het host-token is
niet meer nodig (pull en update-check zijn anoniem), `git push` gaat over SSH,
en `REGISTRY_TOKEN` hoeft niet apart: `release.ps1` en `dev.ps1` vielen daar al
terug op `GITEA_TOKEN`. Geen code-wijziging, alleen minder te beheren
- De tabellen op Eigen Vermogen en Voortgang staan op `table-layout: fixed`,
nodig om ingestelde kolombreedtes te respecteren. Gevolg: lange tekst rekt een
kolom niet meer op maar wordt afgekapt met een ellips. De Opmerkingen-cellen
hebben een `title`, dus hover geeft de volledige tekst
- Het label bij een beschikbare update in "Over VibeFinance" is "Nieuwe versie"
(was "Nieuwste versie" in 1.0.0)
### Opgelost
- De sticky kolomkop van die tabellen was in dark mode maar 30% dekkend: hij
gebruikte `T.card` (`rgba(22,23,27,0.30)`), waardoor de rijen tijdens het
scrollen zichtbaar door de koptekst heen liepen. Nu `T.popupBg`, de
ondoorzichtige variant die al in het thema stond
- In dev bakte Vite de versie uit `frontend/package.json` in in plaats van de
versie uit de root `package.json`. `VITE_APP_VERSION` kwam alleen als
build-arg mee, maar de `development`-stage in de `Dockerfile` declareert die
`ARG` niet — en Vite bakt de constante in bij het *starten* van de dev-server,
niet bij het bouwen van het image. De variabele staat nu in de
runtime-omgeving van de dev-container; `vite.config.js` valt met `||` (was
`??`) terug op `package.json` als hij leeg is
- 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
+92 -16
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.4-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>
--- ---
@@ -30,7 +32,25 @@ Deze applicatie is volledig ontworpen en ontwikkeld met behulp van [Claude.ai](h
De app draait in **2 containers**: `app` (Express API + gebouwde frontend) en De app draait in **2 containers**: `app` (Express API + gebouwde frontend) en
`postgres` (database). `postgres` (database).
**`.env`** — kopieer `.env.example` en vul in: **1. De bestanden op de server zetten.** `docker compose` leest
`docker-compose.yml` uit de map waarin je het commando draait, dus dat bestand
moet er eerst staan. De organisatie in Gitea is publiek, dus dit kan anoniem:
```bash
git clone https://vibehoogie.duckdns.org/Vibe/VibeFinance.git
cd VibeFinance
```
Wil je de repo niet op je server, dan zijn deze twee bestanden genoeg:
```bash
mkdir -p vibefinance && cd vibefinance
base=https://vibehoogie.duckdns.org/Vibe/VibeFinance/raw/branch/main
curl -fsSLO $base/docker-compose.yml
curl -fsSL $base/.env.example -o .env.example
```
**2. `.env` aanmaken** — kopieer `.env.example` en vul in:
```env ```env
POSTGRES_USER=vibefinance POSTGRES_USER=vibefinance
@@ -44,11 +64,26 @@ APP_PORT=3400
openssl rand -hex 64 openssl rand -hex 64
``` ```
**3. Starten:**
```bash ```bash
cp .env.example .env # en vul de waarden in cp .env.example .env # en vul de waarden in
docker compose pull && docker compose up -d docker compose pull && docker compose up -d
``` ```
Twee foutmeldingen en wat ze betekenen:
- **`no configuration file provided: not found`** — je staat in een map zonder
`docker-compose.yml`. Controleer met `ls docker-compose.yml`, of geef het
bestand expliciet mee: `docker compose -f /pad/naar/docker-compose.yml up -d`.
- **`401 Unauthorized`** of **"pull access denied"** — de organisatie staat niet
meer op publiek. Dan alsnog inloggen:
```bash
docker login vibehoogie.duckdns.org # wachtwoord = Gitea-token met read:package
```
Navigeer naar `http://<server-ip>:3400` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol. Navigeer naar `http://<server-ip>:3400` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol.
De container luistert intern op poort 3001 en serveert daar zowel de frontend De container luistert intern op poort 3001 en serveert daar zowel de frontend
@@ -109,13 +144,22 @@ 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
# één Gitea-token met write:package (push) én write:repository (release)
$env:GITEA_TOKEN = "<token met write:package + write:repository>"
.\release.ps1 .\release.ps1
``` ```
Draait de tests, bouwt het app-image voor `amd64` + `arm64` (Raspberry Pi), Draait de tests, bouwt het app-image voor `amd64` + `arm64` (Raspberry Pi),
pusht naar de registry, zet de Git-tag en publiceert de Gitea-release. pusht naar de registry, zet de Git-tag en publiceert de Gitea-release.
Het script regelt ook het bijwerken eromheen: de changelog-kop
`## [Niet vrijgegeven]` wordt `## [x.x.x] - datum`, de versie in
`frontend/package.json` en de badge hierboven gaan mee, en ná het publiceren
zet het de root `package.json` op `x.x.(x+1)-dev` voor de volgende cyclus.
Beschrijft de changelog niets over deze versie, dan stopt het script vóór de
build — anders zou de release met een placeholder als notes online komen.
--- ---
## 🗂️ Functionaliteit ## 🗂️ Functionaliteit
@@ -124,9 +168,19 @@ pusht naar de registry, zet de Git-tag en publiceert de Gitea-release.
|---|---| |---|---|
| 📊 **Dashboard** | Overzicht van totaal eigen vermogen, schulden en netto vermogen | | 📊 **Dashboard** | Overzicht van totaal eigen vermogen, schulden en netto vermogen |
| 💰 **Eigen vermogen** | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek | | 💰 **Eigen vermogen** | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek |
| 📉 **Schulden** | Schulden bijhouden per blok met afbetalingsoverzicht | | 📉 **Schuld** | Schulden bijhouden per blok met afbetalingsoverzicht |
| 📈 **Voortgang** | Grafieken en trends van EV en schuld over tijd (apart bij te houden) | | 📈 **Voortgang Schuld & EV** | Grafieken en trends van EV en schuld over tijd (apart bij te houden) |
| 👥 **Gebruikers** | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) | | 👥 **Gebruikers** | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) |
| ⚙️ **Instellingen** | Licht/donker thema, backup downloaden en backup terugzetten |
De kolommen in de tabellen op Eigen vermogen en Voortgang zijn versleepbaar: pak
de rechterrand van een kolomkop. De breedtes worden per tabel in de browser
bewaard; dubbelklik op de greep voor de standaardbreedte.
Backup en restore lopen via **Instellingen** in de navigatiebalk: "Backup maken"
downloadt een ZIP (`data.json` + `meta.json`) met je eigen financiële data,
"Backup terugzetten" leest zo'n ZIP weer in en schrijft hem via `/api/data` weg.
Dat is de gebruikersvariant; een volledige database-dump staat in `BEHEER.md`.
--- ---
@@ -145,22 +199,33 @@ vibefinance/
├── release.ps1 / release.sh # Release (test, push, tag, Gitea-release) ├── release.ps1 / release.sh # Release (test, push, tag, Gitea-release)
│ │
├── docker/ ├── docker/
│ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container │ ├── dev-entrypoint.sh # Start API + Vite samen in de dev-container
│ ├── check-updates.mjs # Dependency-check tegen het npm-register
│ └── update-exceptions.json # Pakketten die bewust achterblijven, met reden
│
├── 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
│ ├── index.html # Vite-entry, verwijst naar /favicon.svg
│ ├── public/ # Ongebundeld: favicon.svg + icons/
│ └── src/ │ └── src/
│ ├── App.jsx # Root – wiring + idle-logout │ ├── App.jsx # Root – wiring + idle-logout
│ ├── App.css # Reset + losse klassen (o.a. .kolom-grip)
│ ├── 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/ # format.js (bedragen/datums), storage.js
│ ├── hooks/ # useTheme, useUpdateCheck │ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
│ ├── context/ # AppContext (global state + API sync) │ ├── context/ # AppContext (global state + API sync + backup)
│ ├── components/ │ ├── components/
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…) │ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
│ │ ├── LoginPage.jsx │ │ ├── LoginPage.jsx
│ │ ├── LoginBackground.jsx
│ │ ├── NavBar.jsx │ │ ├── NavBar.jsx
│ │ ├── ProfielPopup.jsx │ │ ├── ProfielPopup.jsx
│ │ ├── InstellingenModal.jsx # Thema + backup/restore
│ │ └── GebruikersBeheer.jsx │ │ └── GebruikersBeheer.jsx
│ └── pages/ │ └── pages/
│ ├── DashboardTab.jsx │ ├── DashboardTab.jsx
@@ -173,10 +238,11 @@ vibefinance/
└── src/ └── src/
├── server.js # Entry point: API op /api, SPA op / ├── server.js # Entry point: API op /api, SPA op /
├── db.js # PostgreSQL pool + initDb() schema-migraties ├── db.js # PostgreSQL pool + initDb() schema-migraties
├── logger.js # pino (pino-pretty buiten productie)
├── config/ # Omgevingsvariabelen (incl. STATIC_DIR) ├── config/ # Omgevingsvariabelen (incl. STATIC_DIR)
├── constants/ # Begindata (initialData.js) ├── constants/ # Begindata (initialData.js)
├── middleware/ # auth.js, errorHandler.js ├── middleware/ # auth.js, errorHandler.js
├── routes/ # auth, data, users, health, update-check ├── routes/ # auth, data, users, health, update
└── stores/ # userStore, dataStore └── stores/ # userStore, dataStore
``` ```
@@ -192,9 +258,11 @@ frontend en API.
| Verzoek | Wat de app doet | | Verzoek | Wat de app doet |
|---|---| |---|---|
| `/api/...` | REST API (rate limited) | | `/api/...` | REST API (rate limited, `Cache-Control: no-store`) |
| `/assets/...` | Statische build, 1 jaar `immutable` gecached | | `/assets/...` | Gehashte build-bestanden, 1 jaar `immutable` gecached |
| al het overige | `index.html` — fallback voor React-routing | | `/favicon.svg`, `/icons/...` | Vaste namen, dus `max-age=0, must-revalidate` |
| pad mét extensie, geen bestand | echte 404 (geen HTML waar een icoon wordt verwacht) |
| al het overige | `index.html` — fallback voor React-routing, niet gecached |
--- ---
@@ -242,13 +310,14 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
| `JWT_SECRET` | — | **Verplicht** — minimaal 64 tekens | | `JWT_SECRET` | — | **Verplicht** — minimaal 64 tekens |
| `POSTGRES_USER` | `vibefinance` | Database gebruikersnaam | | `POSTGRES_USER` | `vibefinance` | Database gebruikersnaam |
| `POSTGRES_DB` | `vibefinance` | Database naam | | `POSTGRES_DB` | `vibefinance` | Database naam |
| `DATABASE_URL` | — | Connectiestring; `docker-compose.yml` stelt hem samen uit de drie `POSTGRES_*` waarden, dus zelf zetten hoeft alleen buiten Compose |
| `JWT_EXPIRES` | `7d` | Geldigheidsduur token | | `JWT_EXPIRES` | `7d` | Geldigheidsduur token |
| `CORS_ORIGIN` | `http://localhost` | Externe domeinnaam (één origin, dus zelden nodig) | | `CORS_ORIGIN` | `http://localhost` | Externe domeinnaam (één origin, dus zelden nodig) |
| `APP_PORT` | `3400` | Poort op de host (container luistert op 3001) | | `APP_PORT` | `3400` | Poort op de host (container luistert op 3001) |
| `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` | — | Optioneel: de Gitea-repo is publiek, dus de check werkt anoniem. Alleen nodig als de organisatie weer privé wordt; scope `read:repository` |
--- ---
@@ -256,6 +325,10 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
- Wachtwoorden gehasht met bcrypt (cost factor 12) - Wachtwoorden gehasht met bcrypt (cost factor 12)
- JWT authenticatie met instelbare vervaltijd - JWT authenticatie met instelbare vervaltijd
- Data is per gebruiker: `/api/data` leest en schrijft alleen de rij van de
ingelogde gebruiker (`sub` uit het token)
- Van de rollen wordt alleen **Admin** afgedwongen, op `/api/users` — zie de
sectie Rollen in `BEHEER.md`
- Frontend en API op één origin — geen interne proxy, geen CORS-uitzonderingen - Frontend en API op één origin — geen interne proxy, geen CORS-uitzonderingen
- Content-Security-Policy via Helmet, toegespitst op wat de app laadt - Content-Security-Policy via Helmet, toegespitst op wat de app laadt
- Database bereikbaar alleen binnen het interne Docker netwerk - Database bereikbaar alleen binnen het interne Docker netwerk
@@ -277,7 +350,10 @@ 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**: waarom één Gitea-token op het
werkstation genoeg is, welke scopes het nodig heeft, 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", () => {
+5
View File
@@ -49,6 +49,11 @@ services:
environment: environment:
NODE_ENV: development NODE_ENV: development
PORT: 3001 PORT: 3001
# Vite bakt de versie in bij het STARTEN van de dev-server, dus die moet
# in de runtime-omgeving staan; de build-arg hierboven bereikt alleen de
# frontend-build-stage en doet niets voor target: development.
# Leeg = vite.config.js valt terug op frontend/package.json.
VITE_APP_VERSION: ${VITE_APP_VERSION:-}
JWT_SECRET: dev-secret-change-in-production JWT_SECRET: dev-secret-change-in-production
JWT_EXPIRES: 7d JWT_EXPIRES: 7d
CORS_ORIGIN: http://localhost CORS_ORIGIN: http://localhost
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.4",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

+34
View File
@@ -26,3 +26,37 @@ input[type="number"]::-webkit-inner-spin-button {
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Sleepvlak op de rechterrand van een tabelkop — zie hooks/useKolomBreedtes.js.
Blijft binnen de cel (geen negatieve offset), anders duwt de laatste kolom
een horizontale scrollbar tevoorschijn. */
.kolom-grip {
position: absolute;
top: 0;
right: 0;
width: 6px;
height: 100%;
cursor: col-resize;
user-select: none;
touch-action: none;
}
/* Het streepje erft de kleur van de kop, dus per categorie de accentkleur. */
.kolom-grip::after {
content: "";
position: absolute;
top: 25%;
right: 2px;
width: 1px;
height: 50%;
background: currentColor;
opacity: 0.3;
}
.kolom-grip:hover::after,
.kolom-grip:active::after {
top: 0;
height: 100%;
width: 2px;
opacity: 1;
}
+5 -9
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";
@@ -209,14 +209,10 @@ function AppInner() {
<div style={{ fontSize: 12, color: T.muted, lineHeight: 1.5 }}> <div style={{ fontSize: 12, color: T.muted, lineHeight: 1.5 }}>
v{nieuweVersie.latest} is uitgebracht. Je gebruikt momenteel v{version}. v{nieuweVersie.latest} is uitgebracht. Je gebruikt momenteel v{version}.
</div> </div>
{nieuweVersie.notes && ( {/* De release-notes zelf staan hier niet: dat is Markdown
<div style={{ uit de changelog, en die kwam als ruwe `### `-koppen in
fontSize: 11, color: T.subtext, marginTop: 6, lineHeight: 1.5, een smalle kolom te staan. Wie de details wil, volgt de
maxHeight: 130, overflowY: "auto", whiteSpace: "pre-wrap", link naar de release. */}
}}>
{nieuweVersie.notes}
</div>
)}
{nieuweVersie.url && ( {nieuweVersie.url && (
<a href={nieuweVersie.url} target="_blank" rel="noreferrer" style={{ <a href={nieuweVersie.url} target="_blank" rel="noreferrer" style={{
display: "inline-block", marginTop: 8, display: "inline-block", marginTop: 8,
+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>
+78
View File
@@ -0,0 +1,78 @@
import { useRef, useState } from "react";
import { loadFromStorage, saveToStorage } from "../utils/storage.js";
// Smaller dan dit wordt een kolom onleesbaar; de greep blijft wel pakbaar.
const MIN_BREEDTE = 60;
/**
* Versleepbare kolombreedtes voor een tabel.
*
* `kolommen` is een array van { id, breedte } in weergave-volgorde, waarbij
* `breedte` de standaard in pixels is. De aanpassingen van de gebruiker gaan
* per tabel naar localStorage, zodat ze een tabwissel of herlaad overleven.
* Kolommen die later verschijnen (een categorie die de gebruiker toevoegt)
* krijgen hun standaardbreedte; opgeslagen breedtes van verdwenen kolommen
* blijven ongebruikt staan en storen niets.
*
* De tabel moet `tableLayout: "fixed"` krijgen met een <colgroup>, en als
* `width` de teruggegeven `totaal`. Met `width: "100%"` verdeelt de browser de
* ruimte namelijk proportioneel en levert slepen niet exact de gesleepte
* pixels op.
*
* Geeft terug:
* breedtes px per kolom-id, standaard plus aanpassing
* totaal som van de breedtes, voor de `width` van de tabel
* grip props voor het sleepvlak in de <th> van die kolom
* herstel alle kolommen terug naar hun standaardbreedte
*/
export function useKolomBreedtes(opslagKey, kolommen) {
const [aangepast, setAangepast] = useState(() => loadFromStorage(opslagKey) ?? {});
// Spiegelt `aangepast` zodat het slepen en het opslaan bij het loslaten niet
// van de state-updater afhangen (die mag geen bijwerkingen hebben).
const laatste = useRef(aangepast);
const sleep = useRef(null);
const breedtes = {};
for (const k of kolommen) breedtes[k.id] = aangepast[k.id] ?? k.breedte;
const totaal = kolommen.reduce((som, k) => som + breedtes[k.id], 0);
const zet = (volgende) => { laatste.current = volgende; setAangepast(volgende); };
const grip = (id) => ({
className: "kolom-grip",
role: "separator",
title: "Sleep om de kolom breder of smaller te maken — dubbelklik voor de standaardbreedte",
onPointerDown: (e) => {
// Pointer capture: het slepen loopt door buiten de greep, ook boven de
// rijen of naast de tabel, zonder losse listeners op window.
e.preventDefault();
e.currentTarget.setPointerCapture(e.pointerId);
sleep.current = { id, startX: e.clientX, startBreedte: breedtes[id] };
},
onPointerMove: (e) => {
if (!sleep.current) return;
const { id: kolomId, startX, startBreedte } = sleep.current;
const nieuw = Math.max(MIN_BREEDTE, Math.round(startBreedte + e.clientX - startX));
if (laatste.current[kolomId] === nieuw) return;
zet({ ...laatste.current, [kolomId]: nieuw });
},
onPointerUp: () => {
if (!sleep.current) return;
sleep.current = null;
// Pas bij het loslaten opslaan; tijdens het slepen zou elke pixel een
// localStorage-write kosten.
saveToStorage(opslagKey, laatste.current);
},
onDoubleClick: () => {
// Zonder destructuring-omit: die laat een ongebruikte variabele achter.
const rest = { ...laatste.current };
delete rest[id];
zet(rest);
saveToStorage(opslagKey, rest);
},
});
const herstel = () => { zet({}); saveToStorage(opslagKey, {}); };
return { breedtes, totaal, grip, herstel };
}
+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.
+33 -10
View File
@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useApp } from "../context/AppContext.jsx"; import { useApp } from "../context/AppContext.jsx";
import { fmt, fmtDatum } from "../utils/format.js"; import { fmt, fmtDatum } from "../utils/format.js";
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
import { PURPLE, PURPLE_LIGHT, GREEN, RED, CAT_COLORS } from "../constants/index.js"; import { PURPLE, PURPLE_LIGHT, GREEN, RED, CAT_COLORS } from "../constants/index.js";
import { import {
VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter, VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter,
@@ -250,8 +251,22 @@ function EVVoortgangTabel() {
const removeRow = (datum) => setEvRows(evRows.filter((r) => r.datum !== datum)); const removeRow = (datum) => setEvRows(evRows.filter((r) => r.datum !== datum));
const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", background: T.card, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` }; // Kolommen in weergave-volgorde, met hun standaardbreedte. De categorieën
const td = { padding: "7px 10px", fontSize: 12, color: T.text, borderBottom: `1px solid ${T.border}` }; // zijn dynamisch, dus de lijst wordt per render opgebouwd.
const kolommen = [
{ id: "datum", breedte: 100 },
...cats.map((c) => ({ id: c.id, breedte: 100 })),
{ id: "nettoWaarde", breedte: 115 },
{ id: "verschil", breedte: 105 },
{ id: "opmerkingen", breedte: 150 },
...(!locked ? [{ id: "acties", breedte: 75 }] : []),
];
const { breedtes, totaal, grip } = useKolomBreedtes("ev_kolom_breedtes", kolommen);
// overflow/ellipsis horen bij tableLayout "fixed": de inhoud rekt de kolom
// niet meer op, dus zonder afkappen loopt lange tekst de buurcel binnen.
const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", background: T.popupBg, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` };
const td = { padding: "7px 10px", fontSize: 12, color: T.text, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", borderBottom: `1px solid ${T.border}` };
return ( return (
<div> <div>
@@ -326,15 +341,22 @@ function EVVoortgangTabel() {
{evRows.length > 0 ? ( {evRows.length > 0 ? (
<div style={{ overflowX: "auto" }}> <div style={{ overflowX: "auto" }}>
<div style={{ maxHeight: 340, overflowY: "auto" }}> <div style={{ maxHeight: 340, overflowY: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse", minWidth: 600 }}> <table style={{ width: totaal, borderCollapse: "collapse", tableLayout: "fixed" }}>
<colgroup>
{kolommen.map((k) => <col key={k.id} style={{ width: breedtes[k.id] }} />)}
</colgroup>
<thead> <thead>
<tr> <tr>
<th style={th}>Datum</th> <th style={th}>Datum<span {...grip("datum")} /></th>
{cats.map((c) => <th key={c.id} style={{ ...th, color: c.color }}>{c.naam}</th>)} {cats.map((c) => (
<th style={th}>Netto Waarde</th> <th key={c.id} style={{ ...th, color: c.color }}>
<th style={th}>Verschil</th> {c.naam}<span {...grip(c.id)} />
<th style={th}>Opmerkingen</th> </th>
{!locked && <th style={th}></th>} ))}
<th style={th}>Netto Waarde<span {...grip("nettoWaarde")} /></th>
<th style={th}>Verschil<span {...grip("verschil")} /></th>
<th style={th}>Opmerkingen<span {...grip("opmerkingen")} /></th>
{!locked && <th style={th}><span {...grip("acties")} /></th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -349,7 +371,8 @@ function EVVoortgangTabel() {
{cats.map((c) => <td key={c.id} style={{ ...td, color: c.color }}>{fmt(row[c.id] || 0)}</td>)} {cats.map((c) => <td key={c.id} style={{ ...td, color: c.color }}>{fmt(row[c.id] || 0)}</td>)}
<td style={{ ...td, fontWeight: 700, color: PURPLE_LIGHT }}>{fmt(row.nettoWaarde || 0)}</td> <td style={{ ...td, fontWeight: 700, color: PURPLE_LIGHT }}>{fmt(row.nettoWaarde || 0)}</td>
<td style={{ ...td, fontWeight: 700, color: vColor }}>{vLabel}</td> <td style={{ ...td, fontWeight: 700, color: vColor }}>{vLabel}</td>
<td style={{ ...td, color: T.muted }}>{row.opmerkingen || "—"}</td> {/* title: bij een smalle kolom is de tekst afgekapt */}
<td style={{ ...td, color: T.muted }} title={row.opmerkingen || ""}>{row.opmerkingen || "—"}</td>
{!locked && ( {!locked && (
<td style={td}> <td style={td}>
<div style={{ display: "flex", gap: 6 }}> <div style={{ display: "flex", gap: 6 }}>
+28 -10
View File
@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useApp } from "../context/AppContext.jsx"; import { useApp } from "../context/AppContext.jsx";
import { fmt, fmtDatum } from "../utils/format.js"; import { fmt, fmtDatum } from "../utils/format.js";
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js"; import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js";
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx"; import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx";
@@ -261,8 +262,21 @@ export default function VoortgangTab() {
setData({ ...data, schuld: { ...schuld, schuldHistory: schuldHistory.filter((s) => s.datum !== datum) } }); setData({ ...data, schuld: { ...schuld, schuldHistory: schuldHistory.filter((s) => s.datum !== datum) } });
}; };
const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", background: T.card, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` }; // Kolommen in weergave-volgorde, met hun standaardbreedte.
const td = { padding: "7px 10px", fontSize: 12, color: T.text, borderBottom: `1px solid ${T.border}` }; const kolommen = [
{ id: "datum", breedte: 120 },
{ id: "ev", breedte: 175 },
{ id: "schuld", breedte: 160 },
{ id: "netto", breedte: 175 },
{ id: "opmerkingen", breedte: 340 },
...(!locked ? [{ id: "acties", breedte: 90 }] : []),
];
const { breedtes, totaal, grip } = useKolomBreedtes("voortgang_kolom_breedtes", kolommen);
// overflow/ellipsis horen bij tableLayout "fixed": de inhoud rekt de kolom
// niet meer op, dus zonder afkappen loopt lange tekst de buurcel binnen.
const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", background: T.popupBg, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` };
const td = { padding: "7px 10px", fontSize: 12, color: T.text, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", borderBottom: `1px solid ${T.border}` };
return ( return (
<div> <div>
@@ -396,15 +410,18 @@ export default function VoortgangTab() {
)} )}
{voortgangRows.length > 0 ? ( {voortgangRows.length > 0 ? (
<div style={{ overflowX: "auto" }}> <div style={{ overflowX: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse", minWidth: 560 }}> <table style={{ width: totaal, borderCollapse: "collapse", tableLayout: "fixed" }}>
<colgroup>
{kolommen.map((k) => <col key={k.id} style={{ width: breedtes[k.id] }} />)}
</colgroup>
<thead> <thead>
<tr> <tr>
<th style={th}>Datum</th> <th style={th}>Datum<span {...grip("datum")} /></th>
<th style={{ ...th, color: GREEN }}>Eigen Vermogen</th> <th style={{ ...th, color: GREEN }}>Eigen Vermogen<span {...grip("ev")} /></th>
<th style={{ ...th, color: RED }}>Schuld</th> <th style={{ ...th, color: RED }}>Schuld<span {...grip("schuld")} /></th>
<th style={{ ...th, color: PURPLE_LIGHT }}>Nettowaarde</th> <th style={{ ...th, color: PURPLE_LIGHT }}>Nettowaarde<span {...grip("netto")} /></th>
<th style={th}>Opmerkingen</th> <th style={th}>Opmerkingen<span {...grip("opmerkingen")} /></th>
{!locked && <th style={th}></th>} {!locked && <th style={th}><span {...grip("acties")} /></th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -414,7 +431,8 @@ export default function VoortgangTab() {
<td style={{ ...td, color: GREEN, fontWeight: 600 }}>{row.ev !== null ? fmt(row.ev) : "—"}</td> <td style={{ ...td, color: GREEN, fontWeight: 600 }}>{row.ev !== null ? fmt(row.ev) : "—"}</td>
<td style={{ ...td, color: RED, fontWeight: 600 }}>{row.schuld !== null ? fmt(row.schuld) : "—"}</td> <td style={{ ...td, color: RED, fontWeight: 600 }}>{row.schuld !== null ? fmt(row.schuld) : "—"}</td>
<td style={{ ...td, fontWeight: 700, color: row.netto !== null ? (row.netto >= 0 ? GREEN : RED) : T.muted }}>{row.netto !== null ? fmt(row.netto) : "—"}</td> <td style={{ ...td, fontWeight: 700, color: row.netto !== null ? (row.netto >= 0 ? GREEN : RED) : T.muted }}>{row.netto !== null ? fmt(row.netto) : "—"}</td>
<td style={{ ...td, color: T.muted }}>{row.opmerkingen || "—"}</td> {/* title: bij een smalle kolom is de tekst afgekapt */}
<td style={{ ...td, color: T.muted }} title={row.opmerkingen || ""}>{row.opmerkingen || "—"}</td>
{!locked && ( {!locked && (
<td style={{ ...td, whiteSpace: "nowrap" }}> <td style={{ ...td, whiteSpace: "nowrap" }}>
<button onClick={() => setEditRow(row)} style={{ padding: "2px 8px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 6, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, marginRight: 4 }}>✏️</button> <button onClick={() => setEditRow(row)} style={{ padding: "2px 8px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 6, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, marginRight: 4 }}>✏️</button>
+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
@@ -41,6 +41,6 @@ export default defineConfig({
define: { define: {
// VITE_API_URL wordt ingebakken tijdens build // VITE_API_URL wordt ingebakken tijdens build
__API_URL__: JSON.stringify(process.env.VITE_API_URL ?? "/api"), __API_URL__: JSON.stringify(process.env.VITE_API_URL ?? "/api"),
__APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION ?? pkg.version), __APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION || pkg.version),
}, },
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "vibefinance", "name": "vibefinance",
"version": "0.1.7", "version": "1.0.5",
"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",
+193 -18
View File
@@ -9,6 +9,16 @@
# .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen) # .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
# .\release.ps1 -DryRun # toon alle commando's zonder iets uit te voeren # .\release.ps1 -DryRun # toon alle commando's zonder iets uit te voeren
# .\release.ps1 -Force # sla conflictcontrole over, overschrijf tags # .\release.ps1 -Force # sla conflictcontrole over, overschrijf tags
# .\release.ps1 -NoChangelog # publiceer bewust zonder release-notes
#
# Het script werkt zelf vier dingen bij, dus die hoef je niet met de hand te
# doen: de changelog-kop `## [Niet vrijgegeven]` -> `## [x.x.x] - datum`, de
# versie in frontend/package.json, de versiebadge in README.md, en ná het
# publiceren de versie in de root package.json -> `x.x.(x+1)-dev`.
#
# LET OP bij -Force: dat verwijdert de bestaande tag lokaal en op origin, en
# Gitea gooit de bijbehorende release daarmee weg. Onder -DryRun wordt dat
# alleen gemeld (dat was eerder niet zo — zie stap 4).
# #
# Vereisten: # Vereisten:
# - Docker Desktop actief # - Docker Desktop actief
@@ -16,6 +26,9 @@
# - docker buildx builder actief: docker buildx create --use # - docker buildx builder actief: docker buildx create --use
# - Git geinstalleerd en working directory is de repo root # - Git geinstalleerd en working directory is de repo root
# - root package.json bevat een schoon versienummer ZONDER -dev suffix # - root package.json bevat een schoon versienummer ZONDER -dev suffix
# - Changelog.md beschrijft deze versie, onder `## [Niet vrijgegeven]` of
# onder een kop `## [x.x.x]`; anders breekt het script af (-NoChangelog
# omzeilt dat)
# - GITEA_TOKEN in de omgeving (token met release-rechten) voor stap 5, # - GITEA_TOKEN in de omgeving (token met release-rechten) voor stap 5,
# de Gitea-release waar de update-melding in de app op afgaat # de Gitea-release waar de update-melding in de app op afgaat
@@ -23,11 +36,37 @@ param(
[switch]$NoBuild, [switch]$NoBuild,
[switch]$SkipTests, [switch]$SkipTests,
[switch]$DryRun, [switch]$DryRun,
[switch]$Force [switch]$Force,
# Releasen zonder inhoudelijke release-notes. Zonder deze switch breekt het
# script af wanneer de changelog niets over deze versie zegt.
[switch]$NoChangelog
) )
$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 ──
# ════════════════════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════════════════════
@@ -52,16 +91,13 @@ if ($appVersion -match "-dev$") {
exit 1 exit 1
} }
# ── Versie synchroniseren naar frontend/package.json ────────────────────── # Git-tag met `v`-prefix, net als de release-naam hieronder en net als de tags
$frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json" # t/m v1.0.0. Gitea toont de tag op de release-pagina, dus zonder prefix stond
$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json # daar het kale nummer. De image-tags in de registry houden bewust géén `v`:
if ($frontendPkg.version -ne $appVersion) { # dat is de conventie voor container-tags.
$frontendPkg.version = $appVersion # De update-check verdraagt beide: zowel backend/src/routes/update.js als
[System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10)) # useUpdateCheck.js strippen een leidende `v` voordat ze versies vergelijken.
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green $gitTag = "v$appVersion"
}
$gitTag = "$appVersion"
# ── Registry tags (één image voor frontend + backend) ───────────────────── # ── Registry tags (één image voor frontend + backend) ─────────────────────
$appBase = "$registry/$appImage" $appBase = "$registry/$appImage"
@@ -102,12 +138,13 @@ function Write-Step {
Write-Host "── $Label" -ForegroundColor Cyan Write-Host "── $Label" -ForegroundColor Cyan
} }
# Haalt de sectie van deze versie uit Changelog.md als release-notes. # Haalt de sectie van deze versie uit changelog-tekst. Werkt op een string en
function Get-ChangelogSection { # niet op het bestand, zodat een droogloop kan valideren tegen de kop die hij
param([string]$Version) # zou schrijven zonder die daadwerkelijk te schrijven.
$path = Join-Path $PSScriptRoot "Changelog.md" function Get-ChangelogSectionFromText {
if (-not (Test-Path $path)) { return "" } param([string]$Text, [string]$Version)
$lines = Get-Content $path if (-not $Text) { return "" }
$lines = $Text -split "\r?\n"
$start = -1 $start = -1
$needle = "^##\s*\[" + [regex]::Escape($Version) + "\]" $needle = "^##\s*\[" + [regex]::Escape($Version) + "\]"
for ($i = 0; $i -lt $lines.Count; $i++) { for ($i = 0; $i -lt $lines.Count; $i++) {
@@ -123,6 +160,99 @@ function Get-ChangelogSection {
return (($out -join "`n").Trim()) return (($out -join "`n").Trim())
} }
# ── Changelog-kop vrijgeven ───────────────────────────────────────────────
# De release-notes komen uit de sectie `## [x.x.x]` in Changelog.md. Stond het
# werk nog onder `## [Niet vrijgegeven]`, dan vond het script niets en werd stil
# de placeholder "Release x.x.x" gepubliceerd — dat gebeurde bij 0.1.9 en 1.0.2,
# en is achteraf alleen met een PATCH op de release te herstellen. Daarom zet het
# script de kop nu zelf om, en breekt het af als er niets te melden valt.
# Zelfde regel als bij de andere auto-updates: een droogloop meldt, maar schrijft
# niet.
$changelogPath = Join-Path $PSScriptRoot "Changelog.md"
$nieuweKop = "## [$appVersion] - $(Get-Date -Format 'yyyy-MM-dd')"
$changelogTxt = if (Test-Path $changelogPath) { [System.IO.File]::ReadAllText($changelogPath) } else { "" }
$kopPatroon = "(?m)^##\s*\[" + [regex]::Escape($appVersion) + "\]"
# Niet `\s*$`: `\s` matcht ook newlines, en dan slokt de match de lege regel ná
# de kop op — die verdween daardoor bij het omzetten. En niet `[ \t]*$` alleen:
# dit bestand heeft CRLF (zie .gitattributes), dus `$` staat achter een `\r` die
# geen space of tab is. De lookahead laat het regeleinde buiten de match, zodat
# de CRLF intact blijft.
$onderwegPatroon = "(?m)^##[ \t]*\[[ \t]*Niet vrijgegeven[ \t]*\][ \t]*(?=\r?$)"
# Eerst alleen in het geheugen omzetten. Schrijven gebeurt na de controle
# hieronder: bij een lege `[Niet vrijgegeven]`-sectie zou de kop anders al
# vervangen zijn terwijl het script daarna afbreekt, en dan blijft er een
# aangepaste changelog achter bij een release die niet doorging.
$kopOmzetten = ($changelogTxt -notmatch $kopPatroon -and $changelogTxt -match $onderwegPatroon)
if ($kopOmzetten) {
# $nieuweKop bevat geen '$', dus veilig als vervangingstekst.
$changelogTxt = [regex]::Replace($changelogTxt, $onderwegPatroon, $nieuweKop)
}
if (-not (Get-ChangelogSectionFromText $changelogTxt $appVersion) -and -not $NoChangelog) {
Write-Host ""
Write-Host "Changelog.md zegt niets over $appVersion." -ForegroundColor Red
Write-Host ""
Write-Host "Zonder notes komt er alleen de tekst 'Release $appVersion' in de Gitea-release," -ForegroundColor Yellow
Write-Host "en dat is achteraf alleen met een PATCH op de release te herstellen." -ForegroundColor Yellow
Write-Host ""
Write-Host "Zet je wijzigingen in Changelog.md onder een kop:" -ForegroundColor White
Write-Host " ## [Niet vrijgegeven]" -ForegroundColor White
Write-Host "Die zet dit script bij de volgende run zelf om naar '$nieuweKop'." -ForegroundColor White
Write-Host ""
Write-Host "Bewust zonder release-notes publiceren? Draai met -NoChangelog." -ForegroundColor DarkGray
Write-Host ""
Write-Host "Er is niets gepubliceerd." -ForegroundColor DarkGray
exit 1
}
# De controle is doorstaan, dus nu pas schrijven.
if ($kopOmzetten) {
if ($DryRun) {
Write-Host "[DRY-RUN] Changelog-kop vrijgeven: '## [Niet vrijgegeven]' -> '$nieuweKop'" -ForegroundColor Yellow
} else {
[System.IO.File]::WriteAllText($changelogPath, $changelogTxt)
Write-Host "Changelog-kop vrijgegeven: $nieuweKop" -ForegroundColor Green
}
}
# ── Versie synchroniseren naar frontend/package.json ──────────────────────
# Staat na de changelog-controle: anders laat een afgebroken release wél een
# gewijzigde working tree achter terwijl hij "niets gepubliceerd" meldt.
# Een droogloop schrijft hier niet, om dezelfde reden.
$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
}
}
# ── 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
}
}
}
# ── Samenvatting ─────────────────────────────────────────────────────────── # ── Samenvatting ───────────────────────────────────────────────────────────
Write-Host "" Write-Host ""
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
@@ -316,14 +446,24 @@ if (-not $NoBuild) {
Write-Step "[4/5] Git-tag aanmaken en pushen" Write-Step "[4/5] Git-tag aanmaken en pushen"
if ($Force) { if ($Force) {
# LET OP: deze twee commando's gingen niet via Invoke-Cmd en hadden dus geen
# -DryRun-controle. Een `-DryRun -Force` verwijderde daardoor écht de tag,
# lokaal en op origin — en Gitea ruimt de release op zodra de tag verdwijnt.
# Zo is de release van 1.0.2 verloren gegaan.
if ($DryRun) {
Write-Host "[DRY-RUN] git tag -d $gitTag" -ForegroundColor Yellow
Write-Host "[DRY-RUN] git push origin :refs/tags/$gitTag" -ForegroundColor Yellow
} else {
Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
$ErrorActionPreference = "SilentlyContinue" $ErrorActionPreference = "SilentlyContinue"
git tag -d $gitTag 2>$null | Out-Null git tag -d $gitTag 2>$null | Out-Null
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
Write-Host "Bestaande remote tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow Write-Host "Bestaande remote tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
Write-Host " Gitea verwijdert de bijbehorende release mee." -ForegroundColor DarkGray
$ErrorActionPreference = "SilentlyContinue" $ErrorActionPreference = "SilentlyContinue"
git push origin ":refs/tags/$gitTag" 2>$null | Out-Null git push origin ":refs/tags/$gitTag" 2>$null | Out-Null
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
}
} }
Write-Host "Git-tag aanmaken: $gitTag" -ForegroundColor White Write-Host "Git-tag aanmaken: $gitTag" -ForegroundColor White
@@ -356,7 +496,10 @@ if (-not $releaseApi) {
Write-Host "Geen GITEA_TOKEN in de omgeving — release overgeslagen." -ForegroundColor Yellow Write-Host "Geen GITEA_TOKEN in de omgeving — release overgeslagen." -ForegroundColor Yellow
Write-Host " Zonder release ziet de app geen update-melding voor $appVersion." -ForegroundColor DarkGray Write-Host " Zonder release ziet de app geen update-melding voor $appVersion." -ForegroundColor DarkGray
} else { } else {
$notes = Get-ChangelogSection $appVersion # Uit $changelogTxt en niet opnieuw van schijf: in een droogloop is de kop
# nog niet geschreven, en dan zou de payload hier de placeholder tonen —
# precies wat je met een droogloop wilt uitsluiten.
$notes = Get-ChangelogSectionFromText $changelogTxt $appVersion
if (-not $notes) { $notes = "Release $appVersion" } if (-not $notes) { $notes = "Release $appVersion" }
$payload = @{ $payload = @{
tag_name = $gitTag tag_name = $gitTag
@@ -402,6 +545,38 @@ Write-Host " Git-tag: $gitTag -> origin" -ForegroundColor Green
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
Write-Host "" Write-Host ""
# ── Versie naar de volgende dev-cyclus ────────────────────────────────────
# Was voorheen stap 4 met de hand. Gebeurt hier, ná het publiceren, zodat een
# afgebroken release het versienummer niet vooruit zet. Bijkomend voordeel: de
# `-dev` suffix laat dezelfde release niet nog eens draaien (de controle boven
# in dit script weigert een `-dev` versie), dus een dubbele publicatie is
# meteen uitgesloten. Wil je bewust opnieuw releasen, zet het nummer dan zelf
# terug.
if ($appVersion -match '^(\d+)\.(\d+)\.(\d+)$') {
$volgende = "{0}.{1}.{2}-dev" -f $Matches[1], $Matches[2], ([int]$Matches[3] + 1)
$rootPkgPath = Join-Path $PSScriptRoot "package.json"
if ($DryRun) {
Write-Host "[DRY-RUN] Versie in package.json: $appVersion -> $volgende" -ForegroundColor Yellow
} else {
# Gerichte tekstvervanging in plaats van ConvertTo-Json: dat zou het hele
# bestand herschrijven en de opmaak van de andere velden aantasten.
$rootTxt = [System.IO.File]::ReadAllText($rootPkgPath)
$treffer = [regex]::Match($rootTxt, '"version"\s*:\s*"' + [regex]::Escape($appVersion) + '"')
if ($treffer.Success) {
$rootTxt = $rootTxt.Remove($treffer.Index, $treffer.Length).Insert($treffer.Index, '"version": "' + $volgende + '"')
[System.IO.File]::WriteAllText($rootPkgPath, $rootTxt)
Write-Host "Versie in package.json naar de volgende cyclus: $volgende" -ForegroundColor Green
Write-Host " Commit dit mee, samen met de changelog-kop en de README-badge." -ForegroundColor DarkGray
Write-Host ""
} else {
Write-Host "Kon '$appVersion' niet terugvinden in package.json — versie niet bijgewerkt." -ForegroundColor Yellow
}
}
} else {
Write-Host "Versie '$appVersion' is geen x.y.z — volgende dev-versie niet automatisch gezet." -ForegroundColor Yellow
}
# Expliciete 0: anders erft de caller de exitcode van het laatste # Expliciete 0: anders erft de caller de exitcode van het laatste
# docker-commando (bijv. de registry-check). # docker-commando (bijv. de registry-check).
exit 0 exit 0
+1
View File
@@ -9,6 +9,7 @@
# ./release.sh -SkipTests # sla de testsuite over (niet aanbevolen) # ./release.sh -SkipTests # sla de testsuite over (niet aanbevolen)
# ./release.sh -DryRun # toon alle commando's zonder iets uit te voeren # ./release.sh -DryRun # toon alle commando's zonder iets uit te voeren
# ./release.sh -Force # sla conflictcontrole over, overschrijf tags # ./release.sh -Force # sla conflictcontrole over, overschrijf tags
# ./release.sh -NoChangelog # publiceer bewust zonder release-notes
# #
# Combinaties: # Combinaties:
# ./release.sh -Force -NoBuild # ./release.sh -Force -NoBuild