Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceff734793 | ||
|
|
624f51679a | ||
|
|
5f90331694 | ||
|
|
4e923fa998 | ||
|
|
857322a490 | ||
|
|
6fd4f4766b | ||
|
|
1b14553417 | ||
|
|
e5f78f5368 | ||
|
|
09cc6ae488 | ||
|
|
bff64e5012 | ||
|
|
71cb9b9449 | ||
|
|
4e4a9ca3e1 | ||
|
|
51f876efd3 | ||
|
|
00893fa6be | ||
|
|
b5a068d3d8 | ||
|
|
dbdb3ecfc3 | ||
|
|
2a4edee192 | ||
|
|
fdb9771916 |
@@ -15,3 +15,6 @@ release.ps1
|
||||
release.sh
|
||||
test.ps1
|
||||
test.sh
|
||||
|
||||
# Documentatie-afbeeldingen horen niet in het image
|
||||
docs/
|
||||
|
||||
+5
-1
@@ -17,5 +17,9 @@ CORS_ORIGIN=http://localhost:3400
|
||||
|
||||
# Update-notificaties (leeg = melding uit)
|
||||
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=
|
||||
|
||||
@@ -9,3 +9,7 @@ docker/* text eol=lf
|
||||
|
||||
# PowerShell mag CRLF houden (Windows-tooling)
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Afbeeldingen nooit door de regeleinde-normalisatie halen
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ frontend/dist/
|
||||
|
||||
# Omgevingsvariabelen (gebruik .env.example als sjabloon)
|
||||
.env
|
||||
.env.local
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Logs en rommel
|
||||
*.log
|
||||
|
||||
@@ -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`).
|
||||
> `.\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
|
||||
@@ -74,27 +93,199 @@ 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
|
||||
|
||||
```powershell
|
||||
# 1. Versienummer in package.json van "x.x.x-dev" naar "x.x.x"
|
||||
# 2. Changelog.md bijwerken — die sectie wordt de release-notes
|
||||
# 3. Token met release-rechten zetten
|
||||
$env:GITEA_TOKEN = "<token>"
|
||||
# 2. Wijzigingen in Changelog.md onder de kop "## [Niet vrijgegeven]"
|
||||
# (de kop zelf omzetten hoeft niet — dat doet het script)
|
||||
# 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 -DryRun # droogloop — toont alle stappen zonder uit te voeren
|
||||
.\release.ps1 -NoBuild # sla docker build over, alleen tag + release
|
||||
.\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||
.\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
|
||||
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 |
|
||||
| `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
|
||||
@@ -107,6 +298,14 @@ docker compose pull && 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
|
||||
|
||||
Bij een upgrade van PostgreSQL (bijv. 16 → 18) is de bestaande data niet compatibel. Stappen:
|
||||
@@ -280,17 +479,14 @@ Inloggen op de registry:
|
||||
docker login vibehoogie.duckdns.org
|
||||
```
|
||||
|
||||
Gebruikersnaam is je Gitea-account, wachtwoord een **access token** met scope
|
||||
`write:package` (Gitea → Settings → Applications → Generate New Token). Zonder
|
||||
login faalt een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push`
|
||||
controleren dit vóór de build en stoppen dan meteen.
|
||||
Gebruikersnaam is je Gitea-account, wachtwoord het **access token** van deze
|
||||
machine — `vibefinance-workstation`, zie [Tokens](#tokens). Zonder login faalt
|
||||
een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push` controleren dit
|
||||
vóór de build en stoppen dan meteen.
|
||||
|
||||
Liever niet handmatig inloggen? Zet deze variabelen, dan logt het script zelf in:
|
||||
|
||||
```powershell
|
||||
$env:REGISTRY_USER = "<gitea-gebruiker>"
|
||||
$env:REGISTRY_TOKEN = "<gitea-token met write:package>"
|
||||
```
|
||||
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
|
||||
nodig als je daar bewust een ánder token voor wilt gebruiken.
|
||||
|
||||
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
|
||||
@@ -302,6 +498,35 @@ Controleren wat er is opgeslagen:
|
||||
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
|
||||
@@ -319,7 +544,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).
|
||||
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**
|
||||
|
||||
@@ -336,16 +597,17 @@ uit `.env` (of de default in `docker-compose.yml`). Beschikbare backend-variabel
|
||||
| Variabele | Betekenis |
|
||||
|---|---|
|
||||
| `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 kale Git-tag is niet genoeg: `releases/latest` geeft alleen echte releases terug.
|
||||
`release.ps1` maakt die aan (stap 4) met de changelog-sectie van die versie als
|
||||
release-notes. Daarvoor is 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
|
||||
$env:GITEA_TOKEN = "<token>"
|
||||
$env:GITEA_TOKEN = "<vibefinance-workstation>" # of gewoon uit .env.release
|
||||
.\release.ps1
|
||||
```
|
||||
|
||||
|
||||
+167
@@ -1,5 +1,172 @@
|
||||
# Changelog
|
||||
|
||||
## [Niet vrijgegeven]
|
||||
|
||||
### 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
|
||||
|
||||
### Gewijzigd
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
|
||||
Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<img src="docs/screenshot-dashboard.png" alt="VibeFinance dashboard" width="820">
|
||||
|
||||
</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
|
||||
`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
|
||||
POSTGRES_USER=vibefinance
|
||||
@@ -44,11 +64,26 @@ APP_PORT=3400
|
||||
openssl rand -hex 64
|
||||
```
|
||||
|
||||
**3. Starten:**
|
||||
|
||||
```bash
|
||||
cp .env.example .env # en vul de waarden in
|
||||
|
||||
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.
|
||||
|
||||
De container luistert intern op poort 3001 en serveert daar zowel de frontend
|
||||
@@ -109,13 +144,22 @@ backend/package.json (15 dependencies)
|
||||
### Productie release
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -128,6 +172,10 @@ pusht naar de registry, zet de Git-tag en publiceert de Gitea-release.
|
||||
| 📈 **Voortgang** | Grafieken en trends van EV en schuld over tijd (apart bij te houden) |
|
||||
| 👥 **Gebruikers** | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) |
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architectuur
|
||||
@@ -147,14 +195,19 @@ vibefinance/
|
||||
├── docker/
|
||||
│ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container
|
||||
│
|
||||
├── docs/
|
||||
│ └── screenshot-dashboard.png # Screenshot bovenaan deze README
|
||||
│
|
||||
├── frontend/ # React / Vite SPA (build → dist)
|
||||
│ ├── vite.config.js # /api proxy naar de API in dezelfde container
|
||||
│ └── src/
|
||||
│ ├── App.jsx # Root – wiring + idle-logout
|
||||
│ ├── App.css # Reset + losse klassen (o.a. .kolom-grip)
|
||||
│ ├── main.jsx
|
||||
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
|
||||
│ ├── constants/ # Kleuren, tabs, begindata
|
||||
│ ├── utils/ # Formatters, helpers
|
||||
│ ├── hooks/ # useTheme, useUpdateCheck
|
||||
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
|
||||
│ ├── context/ # AppContext (global state + API sync)
|
||||
│ ├── components/
|
||||
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
|
||||
@@ -248,7 +301,7 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
|
||||
| `PORT` | `3001` | Poort binnen de container |
|
||||
| `STATIC_DIR` | `/app/public` | Map met de frontend-build |
|
||||
| `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` |
|
||||
|
||||
---
|
||||
|
||||
@@ -277,7 +330,10 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
|
||||
|
||||
## 🛠️ 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
@@ -5,7 +5,7 @@ import rateLimit from "express-rate-limit";
|
||||
import cookieParser from "cookie-parser";
|
||||
import pinoHttp from "pino-http";
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { extname, join, relative, sep } from "path";
|
||||
import { config } from "./config/index.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { initDb } from "./db.js";
|
||||
@@ -56,6 +56,17 @@ if (config.NODE_ENV !== "test") {
|
||||
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 ────────────────────────────────────────────────────────────────
|
||||
app.use("/api/health", healthRouter);
|
||||
app.use("/api/update-check", updateRouter);
|
||||
@@ -73,21 +84,34 @@ const indexHtml = join(config.STATIC_DIR, "index.html");
|
||||
export const servesFrontend = existsSync(indexHtml);
|
||||
|
||||
if (servesFrontend) {
|
||||
// Assets hebben een hash in de naam en mogen lang gecached worden;
|
||||
// index.html juist nooit, anders blijft een oude build hangen.
|
||||
// Alleen de bundels in /assets krijgen bij elke build een nieuwe, gehashte
|
||||
// naam — die mogen een jaar blijven staan. Bestanden met een vaste naam
|
||||
// (favicon.svg, icons/*) moeten revalideren: met "immutable" blijft een
|
||||
// eenmaal verkeerd gecacht antwoord een jaar hangen, ook na een reload.
|
||||
app.use(express.static(config.STATIC_DIR, {
|
||||
index: false,
|
||||
setHeaders(res, path) {
|
||||
res.setHeader("Cache-Control", path.endsWith(".html")
|
||||
? "no-store, no-cache, must-revalidate"
|
||||
: "public, max-age=31536000, immutable");
|
||||
setHeaders(res, bestandspad) {
|
||||
const relatief = relative(config.STATIC_DIR, bestandspad).split(sep).join("/");
|
||||
if (bestandspad.endsWith(".html")) {
|
||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
} else if (relatief.startsWith("assets/")) {
|
||||
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||
} else {
|
||||
res.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// SPA-fallback: alle overige GET-routes naar index.html. Pathloze middleware
|
||||
// in plaats van app.get("*") — dat patroon is in Express 5 ongeldig.
|
||||
//
|
||||
// Paden met een extensie vragen om een bestand (/favicon.ico, /robots.txt)
|
||||
// en krijgen een echte 404. Anders levert zo'n verzoek index.html op met
|
||||
// status 200 en content-type text/html: de browser krijgt HTML waar hij een
|
||||
// icoon verwacht en toont dan niets, zonder zichtbare fout.
|
||||
app.use((req, res, next) => {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
||||
if (extname(req.path)) return next();
|
||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
res.sendFile(indexHtml);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import { join } from "node:path";
|
||||
|
||||
const INDEX_MARKER = "<div id=\"root\"></div>";
|
||||
const ASSET_BODY = "console.log('vibefinance');";
|
||||
const FAVICON_BODY = "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
|
||||
|
||||
let server;
|
||||
let basis;
|
||||
@@ -23,6 +24,7 @@ before(async () => {
|
||||
mkdirSync(join(staticDir, "assets"));
|
||||
writeFileSync(join(staticDir, "index.html"), `<!DOCTYPE html><html><body>${INDEX_MARKER}</body></html>`);
|
||||
writeFileSync(join(staticDir, "assets", "index-abc12345.js"), ASSET_BODY);
|
||||
writeFileSync(join(staticDir, "favicon.svg"), FAVICON_BODY);
|
||||
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.STATIC_DIR = staticDir;
|
||||
@@ -66,6 +68,15 @@ describe("API", () => {
|
||||
assert.equal(body.latest, null);
|
||||
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", () => {
|
||||
@@ -93,6 +104,32 @@ describe("Frontend serveren", () => {
|
||||
assert.equal(await res.text(), ASSET_BODY);
|
||||
assert.match(res.headers.get("cache-control") ?? "", /immutable/);
|
||||
});
|
||||
|
||||
it("favicon wordt geserveerd als svg", async () => {
|
||||
const res = await fetch(`${basis}/favicon.svg`);
|
||||
assert.equal(res.status, 200);
|
||||
assert.match(res.headers.get("content-type") ?? "", /image\/svg\+xml/);
|
||||
assert.equal(await res.text(), FAVICON_BODY);
|
||||
});
|
||||
|
||||
// De naam bevat geen hash, dus een jaar "immutable" zou een oude of
|
||||
// verkeerde versie onhoudbaar lang in de browsercache vastzetten.
|
||||
it("favicon wordt niet immutable gecached", async () => {
|
||||
const res = await fetch(`${basis}/favicon.svg`);
|
||||
const cache = res.headers.get("cache-control") ?? "";
|
||||
assert.doesNotMatch(cache, /immutable/);
|
||||
assert.match(cache, /must-revalidate/);
|
||||
});
|
||||
|
||||
// Anders krijgt de browser index.html met status 200 en content-type
|
||||
// text/html waar hij een icoon verwacht, en toont hij stil niets.
|
||||
it("ontbrekend bestand geeft 404 in plaats van index.html", async () => {
|
||||
for (const pad of ["/favicon.ico", "/apple-touch-icon.png"]) {
|
||||
const res = await fetch(`${basis}${pad}`);
|
||||
assert.equal(res.status, 404, pad);
|
||||
assert.doesNotMatch(await res.text(), new RegExp(INDEX_MARKER), pad);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Security headers", () => {
|
||||
|
||||
@@ -49,6 +49,11 @@ services:
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
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_EXPIRES: 7d
|
||||
CORS_ORIGIN: http://localhost
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
@@ -13,7 +13,8 @@ export default [
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
globals: { ...globals.browser },
|
||||
// Door Vite ingebakken tijdens de build (define in vite.config.js)
|
||||
globals: { ...globals.browser, __APP_VERSION__: "readonly" },
|
||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||
},
|
||||
settings: { react: { version: "detect" } },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vibefinance-frontend",
|
||||
"version": "0.1.6",
|
||||
"version": "1.0.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -26,3 +26,37 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
@keyframes spin {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SchuldTab from "./pages/SchuldTab.jsx";
|
||||
import VoortgangTab from "./pages/VoortgangTab.jsx";
|
||||
import { RED, PURPLE_LIGHT } from "./constants/index.js";
|
||||
import { fmt } from "./utils/format.js";
|
||||
import { version } from "../package.json";
|
||||
import { version } from "./version.js";
|
||||
import { useUpdateCheck } from "./hooks/useUpdateCheck.js";
|
||||
import ProfielPopup from "./components/ProfielPopup.jsx";
|
||||
import InstellingenModal from "./components/InstellingenModal.jsx";
|
||||
@@ -209,14 +209,10 @@ function AppInner() {
|
||||
<div style={{ fontSize: 12, color: T.muted, lineHeight: 1.5 }}>
|
||||
v{nieuweVersie.latest} is uitgebracht. Je gebruikt momenteel v{version}.
|
||||
</div>
|
||||
{nieuweVersie.notes && (
|
||||
<div style={{
|
||||
fontSize: 11, color: T.subtext, marginTop: 6, lineHeight: 1.5,
|
||||
maxHeight: 130, overflowY: "auto", whiteSpace: "pre-wrap",
|
||||
}}>
|
||||
{nieuweVersie.notes}
|
||||
</div>
|
||||
)}
|
||||
{/* De release-notes zelf staan hier niet: dat is Markdown
|
||||
uit de changelog, en die kwam als ruwe `### `-koppen in
|
||||
een smalle kolom te staan. Wie de details wil, volgt de
|
||||
link naar de release. */}
|
||||
{nieuweVersie.url && (
|
||||
<a href={nieuweVersie.url} target="_blank" rel="noreferrer" style={{
|
||||
display: "inline-block", marginTop: 8,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { TABS } from "../constants/index.js";
|
||||
import { version } from "../../package.json";
|
||||
import { version } from "../version.js";
|
||||
import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
|
||||
import { useApp } from "../context/AppContext.jsx";
|
||||
|
||||
@@ -165,7 +165,7 @@ function OverPopup({ onClose, nieuweVersie, T }) {
|
||||
|
||||
{nieuweVersie && (
|
||||
<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" }}>
|
||||
v{nieuweVersie.latest}
|
||||
</span>
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { version } from "../../package.json";
|
||||
import { version } from "../version.js";
|
||||
|
||||
// De check loopt via de eigen backend (/api/update-check): de Gitea API stuurt
|
||||
// geen CORS-headers, dus de browser mag die niet direct bevragen.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useApp } from "../context/AppContext.jsx";
|
||||
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 {
|
||||
VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter,
|
||||
@@ -250,8 +251,22 @@ function EVVoortgangTabel() {
|
||||
|
||||
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}` };
|
||||
const td = { padding: "7px 10px", fontSize: 12, color: T.text, borderBottom: `1px solid ${T.border}` };
|
||||
// Kolommen in weergave-volgorde, met hun standaardbreedte. De categorieën
|
||||
// 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 (
|
||||
<div>
|
||||
@@ -326,15 +341,22 @@ function EVVoortgangTabel() {
|
||||
{evRows.length > 0 ? (
|
||||
<div style={{ overflowX: "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>
|
||||
<tr>
|
||||
<th style={th}>Datum</th>
|
||||
{cats.map((c) => <th key={c.id} style={{ ...th, color: c.color }}>{c.naam}</th>)}
|
||||
<th style={th}>Netto Waarde</th>
|
||||
<th style={th}>Verschil</th>
|
||||
<th style={th}>Opmerkingen</th>
|
||||
{!locked && <th style={th}></th>}
|
||||
<th style={th}>Datum<span {...grip("datum")} /></th>
|
||||
{cats.map((c) => (
|
||||
<th key={c.id} style={{ ...th, color: c.color }}>
|
||||
{c.naam}<span {...grip(c.id)} />
|
||||
</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>
|
||||
</thead>
|
||||
<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>)}
|
||||
<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, 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 && (
|
||||
<td style={td}>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useApp } from "../context/AppContext.jsx";
|
||||
import { fmt, fmtDatum } from "../utils/format.js";
|
||||
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
|
||||
import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js";
|
||||
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) } });
|
||||
};
|
||||
|
||||
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}` };
|
||||
const td = { padding: "7px 10px", fontSize: 12, color: T.text, borderBottom: `1px solid ${T.border}` };
|
||||
// Kolommen in weergave-volgorde, met hun standaardbreedte.
|
||||
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 (
|
||||
<div>
|
||||
@@ -396,15 +410,18 @@ export default function VoortgangTab() {
|
||||
)}
|
||||
{voortgangRows.length > 0 ? (
|
||||
<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>
|
||||
<tr>
|
||||
<th style={th}>Datum</th>
|
||||
<th style={{ ...th, color: GREEN }}>Eigen Vermogen</th>
|
||||
<th style={{ ...th, color: RED }}>Schuld</th>
|
||||
<th style={{ ...th, color: PURPLE_LIGHT }}>Nettowaarde</th>
|
||||
<th style={th}>Opmerkingen</th>
|
||||
{!locked && <th style={th}></th>}
|
||||
<th style={th}>Datum<span {...grip("datum")} /></th>
|
||||
<th style={{ ...th, color: GREEN }}>Eigen Vermogen<span {...grip("ev")} /></th>
|
||||
<th style={{ ...th, color: RED }}>Schuld<span {...grip("schuld")} /></th>
|
||||
<th style={{ ...th, color: PURPLE_LIGHT }}>Nettowaarde<span {...grip("netto")} /></th>
|
||||
<th style={th}>Opmerkingen<span {...grip("opmerkingen")} /></th>
|
||||
{!locked && <th style={th}><span {...grip("acties")} /></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<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: 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, 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 && (
|
||||
<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>
|
||||
|
||||
@@ -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__;
|
||||
@@ -41,6 +41,6 @@ export default defineConfig({
|
||||
define: {
|
||||
// VITE_API_URL wordt ingebakken tijdens build
|
||||
__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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vibefinance",
|
||||
"version": "0.1.7",
|
||||
"version": "1.0.3",
|
||||
"description": "VibeFinance — persoonlijk vermogensbeheer",
|
||||
"private": true,
|
||||
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
|
||||
|
||||
+181
-17
@@ -9,6 +9,16 @@
|
||||
# .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||
# .\release.ps1 -DryRun # toon alle commando's zonder iets uit te voeren
|
||||
# .\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:
|
||||
# - Docker Desktop actief
|
||||
@@ -16,6 +26,9 @@
|
||||
# - docker buildx builder actief: docker buildx create --use
|
||||
# - Git geinstalleerd en working directory is de repo root
|
||||
# - 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,
|
||||
# de Gitea-release waar de update-melding in de app op afgaat
|
||||
|
||||
@@ -23,11 +36,37 @@ param(
|
||||
[switch]$NoBuild,
|
||||
[switch]$SkipTests,
|
||||
[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"
|
||||
|
||||
# ── 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 ──
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -52,15 +91,6 @@ if ($appVersion -match "-dev$") {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Versie synchroniseren naar frontend/package.json ──────────────────────
|
||||
$frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json"
|
||||
$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json
|
||||
if ($frontendPkg.version -ne $appVersion) {
|
||||
$frontendPkg.version = $appVersion
|
||||
[System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10))
|
||||
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green
|
||||
}
|
||||
|
||||
$gitTag = "$appVersion"
|
||||
|
||||
# ── Registry tags (één image voor frontend + backend) ─────────────────────
|
||||
@@ -102,12 +132,13 @@ function Write-Step {
|
||||
Write-Host "── $Label" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Haalt de sectie van deze versie uit Changelog.md als release-notes.
|
||||
function Get-ChangelogSection {
|
||||
param([string]$Version)
|
||||
$path = Join-Path $PSScriptRoot "Changelog.md"
|
||||
if (-not (Test-Path $path)) { return "" }
|
||||
$lines = Get-Content $path
|
||||
# Haalt de sectie van deze versie uit changelog-tekst. Werkt op een string en
|
||||
# niet op het bestand, zodat een droogloop kan valideren tegen de kop die hij
|
||||
# zou schrijven zonder die daadwerkelijk te schrijven.
|
||||
function Get-ChangelogSectionFromText {
|
||||
param([string]$Text, [string]$Version)
|
||||
if (-not $Text) { return "" }
|
||||
$lines = $Text -split "\r?\n"
|
||||
$start = -1
|
||||
$needle = "^##\s*\[" + [regex]::Escape($Version) + "\]"
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
@@ -123,6 +154,94 @@ function Get-ChangelogSection {
|
||||
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) + "\]"
|
||||
$onderwegPatroon = "(?m)^##\s*\[\s*Niet vrijgegeven\s*\]\s*$"
|
||||
|
||||
# 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 ───────────────────────────────────────────────────────────
|
||||
Write-Host ""
|
||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||
@@ -316,15 +435,25 @@ if (-not $NoBuild) {
|
||||
Write-Step "[4/5] Git-tag aanmaken en pushen"
|
||||
|
||||
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
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
git tag -d $gitTag 2>$null | Out-Null
|
||||
$ErrorActionPreference = "Stop"
|
||||
Write-Host "Bestaande remote tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
|
||||
Write-Host " Gitea verwijdert de bijbehorende release mee." -ForegroundColor DarkGray
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
git push origin ":refs/tags/$gitTag" 2>$null | Out-Null
|
||||
$ErrorActionPreference = "Stop"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Git-tag aanmaken: $gitTag" -ForegroundColor White
|
||||
Invoke-Cmd @("git", "tag", "-a", $gitTag, "-m", "Release $gitTag")
|
||||
@@ -356,7 +485,10 @@ if (-not $releaseApi) {
|
||||
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
|
||||
} 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" }
|
||||
$payload = @{
|
||||
tag_name = $gitTag
|
||||
@@ -402,6 +534,38 @@ Write-Host " Git-tag: $gitTag -> origin" -ForegroundColor Green
|
||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||
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
|
||||
# docker-commando (bijv. de registry-check).
|
||||
exit 0
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# ./release.sh -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||
# ./release.sh -DryRun # toon alle commando's zonder iets uit te voeren
|
||||
# ./release.sh -Force # sla conflictcontrole over, overschrijf tags
|
||||
# ./release.sh -NoChangelog # publiceer bewust zonder release-notes
|
||||
#
|
||||
# Combinaties:
|
||||
# ./release.sh -Force -NoBuild
|
||||
|
||||
Reference in New Issue
Block a user