Compare commits
9
Commits
v0.1.9
..
5f90331694
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f90331694 | ||
|
|
4e923fa998 | ||
|
|
857322a490 | ||
|
|
6fd4f4766b | ||
|
|
1b14553417 | ||
|
|
e5f78f5368 | ||
|
|
09cc6ae488 | ||
|
|
bff64e5012 | ||
|
|
71cb9b9449 |
@@ -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
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,15 +93,115 @@ 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.
|
||||||
|
- **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 of verlopen. Op de registry: helemaal niet ingelogd. |
|
||||||
|
| 403 | Het token mag de repo wél zien, maar mist de **scope** voor dit endpoint. Gitea noemt de vereiste scope in de response-body. |
|
||||||
|
| 404 | Het token mag de repo niet zien. Gitea geeft geen 403, om niet te verklappen dat iets bestaat: verkeerd account, of geen toegang tot de organisatie. Nu de organisatie public is, wijst een 404 op een leesactie eerder op een verkeerde URL dan op rechten. |
|
||||||
|
|
||||||
|
Scopes van een bestaand token zijn in Gitea niet aan te passen. Bij een 403 maak
|
||||||
|
je dus een nieuw token aan; bijwerken kan niet.
|
||||||
|
|
||||||
|
Nakijken wie een token is en wat hij ziet:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$h = @{ Authorization = "token $env:GITEA_TOKEN" }
|
||||||
|
(Invoke-RestMethod https://vibehoogie.duckdns.org/api/v1/user -Headers $h).login
|
||||||
|
(Invoke-RestMethod https://vibehoogie.duckdns.org/api/v1/user/repos -Headers $h).full_name
|
||||||
|
```
|
||||||
|
|
||||||
|
Vanuit de app-container op de host valt de body van een fout anders weg —
|
||||||
|
busybox-`wget` gooit die bij een foutstatus weg, en Node zit in het image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Anoniem, zoals de app het doet zonder UPDATE_CHECK_TOKEN
|
||||||
|
docker exec vibefinance_app node -e 'fetch(process.env.UPDATE_CHECK_URL,{headers:{Accept:"application/json"}}).then(async r=>console.log(r.status, (await r.text()).slice(0,300)))'
|
||||||
|
|
||||||
|
# Mét token, precies zoals routes/update.js de header opbouwt
|
||||||
|
docker exec vibefinance_app node -e 'const t=process.env.UPDATE_CHECK_TOKEN; fetch(process.env.UPDATE_CHECK_URL,{headers:{Accept:"application/json",...(t&&{Authorization:`token ${t}`})}}).then(async r=>console.log(r.status, (await r.text()).slice(0,300)))'
|
||||||
|
```
|
||||||
|
|
||||||
|
> Let op: een **ongeldig** token geeft `401`, ook op deze publieke repo — Gitea
|
||||||
|
> valt bij een verkeerd token niet terug op anonieme toegang. Krijg je 401
|
||||||
|
> terwijl anoniem `200` geeft, haal `UPDATE_CHECK_TOKEN` dan uit `.env` in plaats
|
||||||
|
> van hem te vervangen. Een *leeg* token is onschuldig: `Authorization: token `
|
||||||
|
> negeert Gitea (`200`), en `config/index.js` zet met `|| null` bij een lege
|
||||||
|
> waarde helemaal geen header. Nagemeten op 2026-09-10:
|
||||||
|
> geen header `200`, `token ongeldig123` `401`, leeg `200`.
|
||||||
|
|
||||||
|
### 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. Changelog.md: kop van "Niet vrijgegeven" naar "[x.x.x] - datum"
|
||||||
# 3. Token met release-rechten zetten
|
# 3. Token staat in .env.release (zie Tokens) — of eenmalig in de shell:
|
||||||
$env:GITEA_TOKEN = "<token>"
|
$env:GITEA_TOKEN = "<vibefinance-workstation>"
|
||||||
|
|
||||||
.\release.ps1 # test, bouw (amd64+arm64), push, tag, Gitea-release
|
.\release.ps1 # test, bouw (amd64+arm64), push, tag, Gitea-release
|
||||||
.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren
|
.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren
|
||||||
@@ -95,41 +214,43 @@ $env:GITEA_TOKEN = "<token>"
|
|||||||
Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde
|
Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde
|
||||||
tag op een Raspberry Pi en op een x86-server werkt.
|
tag op een Raspberry Pi en op een x86-server werkt.
|
||||||
|
|
||||||
### Tokens niet elke sessie opnieuw zetten
|
**Wat het script zelf bijwerkt** — met de hand aanpassen is dus dubbel werk:
|
||||||
|
|
||||||
|
| Bestand | Wat |
|
||||||
|
|---|---|
|
||||||
|
| `frontend/package.json` | versie gelijkgetrokken met de root `package.json` |
|
||||||
|
| `README.md` | de `versie-x.x.x`-badge bovenaan |
|
||||||
|
|
||||||
|
Beide gebeuren vóór de build, en een `-DryRun` meldt ze zonder te schrijven. Ze
|
||||||
|
blijven na een echte release als wijziging in je working tree staan; neem ze mee
|
||||||
|
in de volgende commit.
|
||||||
|
|
||||||
|
> **De changelog-kop moet exact `## [x.x.x]` zijn.** `release.ps1` zoekt die kop
|
||||||
|
> om de release-notes te vullen. Staat je werk nog onder `## [Niet vrijgegeven]`,
|
||||||
|
> dan vindt het script niets en valt het terug op de tekst `"Release x.x.x"` —
|
||||||
|
> dan staat er geen inhoudelijke release-notes in Gitea en ziet een gebruiker in
|
||||||
|
> het belletje een placeholder. Dat overkwam 0.1.9. Achteraf repareren kan met
|
||||||
|
> een `PATCH` op `/repos/<org>/<repo>/releases/<id>`, maar vóór de release
|
||||||
|
> omzetten is een stuk minder werk.
|
||||||
|
|
||||||
|
### `.env.release` — token niet elke sessie opnieuw zetten
|
||||||
|
|
||||||
Staat er een `.env.release` naast `release.ps1`, dan leest het script die in
|
Staat er een `.env.release` naast `release.ps1`, dan leest het script die in
|
||||||
voordat er iets gebeurt. Handig voor stap 3, die anders elke nieuwe shell
|
voordat er iets gebeurt, en hoeft stap 3 niet meer met de hand:
|
||||||
opnieuw met de hand moet:
|
|
||||||
|
|
||||||
```
|
```ini
|
||||||
# .env.release — staat in .gitignore, hoort alleen op de release-machine
|
# .env.release — staat in .gitignore, hoort alleen op de release-machine
|
||||||
GITEA_TOKEN=<token met write:repository>
|
GITEA_TOKEN=<vibefinance-workstation>
|
||||||
REGISTRY_USER=<gitea-gebruiker>
|
REGISTRY_USER=<gitea-gebruiker>
|
||||||
REGISTRY_TOKEN=<token met write:package>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Een variabele die al in je shell staat wint, zodat je per run kunt afwijken
|
`REGISTRY_TOKEN` hoeft er niet in: beide scripts vallen bij een ontbrekende
|
||||||
zonder het bestand aan te passen. Het script logt alleen welke namen het heeft
|
`REGISTRY_TOKEN` terug op `GITEA_TOKEN`. Zie [Tokens](#tokens) voor de scopes en
|
||||||
geladen, nooit de waarden.
|
voor wat een 401, 403 of 404 van Gitea betekent.
|
||||||
|
|
||||||
Twee **verschillende** tokens is hier de bedoeling: de registry heeft
|
Een variabele die al in je shell staat wint, zodat je per run kunt afwijken zonder
|
||||||
`write:package` nodig en het aanmaken van een release `write:repository`. Eén
|
het bestand aan te passen. Het script logt alleen welke namen het heeft geladen,
|
||||||
breed token voor beide werkt ook, maar dan gaat een repo-schrijfrecht mee bij
|
nooit de waarden.
|
||||||
elke `docker push`.
|
|
||||||
|
|
||||||
> Een **404** in stap 5 betekent bijna altijd dat de token de repo niet mag
|
|
||||||
> zien: Gitea geeft op repo-endpoints 404 in plaats van 403, om niet te
|
|
||||||
> verklappen dat iets bestaat. Controleer dan eerst wie de token is en wat hij
|
|
||||||
> ziet:
|
|
||||||
>
|
|
||||||
> ```powershell
|
|
||||||
> $h = @{ Authorization = "token $env:GITEA_TOKEN" }
|
|
||||||
> (Invoke-RestMethod https://vibehoogie.duckdns.org/api/v1/user -Headers $h).login
|
|
||||||
> (Invoke-RestMethod https://vibehoogie.duckdns.org/api/v1/user/repos -Headers $h).full_name
|
|
||||||
> ```
|
|
||||||
>
|
|
||||||
> Staat de repo niet in die lijst, dan mist de token de repository-scope of
|
|
||||||
> hoort hij bij een account zonder toegang.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -319,17 +440,14 @@ Inloggen op de registry:
|
|||||||
docker login vibehoogie.duckdns.org
|
docker login vibehoogie.duckdns.org
|
||||||
```
|
```
|
||||||
|
|
||||||
Gebruikersnaam is je Gitea-account, wachtwoord een **access token** met scope
|
Gebruikersnaam is je Gitea-account, wachtwoord het **access token** van deze
|
||||||
`write:package` (Gitea → Settings → Applications → Generate New Token). Zonder
|
machine — `vibefinance-workstation`, zie [Tokens](#tokens). Zonder login faalt
|
||||||
login faalt een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push`
|
een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push` controleren dit
|
||||||
controleren dit vóór de build en stoppen dan meteen.
|
vóór de build en stoppen dan meteen.
|
||||||
|
|
||||||
Liever niet handmatig inloggen? Zet deze variabelen, dan logt het script zelf in:
|
Liever niet handmatig inloggen? Zet `REGISTRY_USER` 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
|
||||||
@@ -343,20 +461,8 @@ docker-credential-desktop list
|
|||||||
|
|
||||||
### Pullen op een deploy-machine
|
### Pullen op een deploy-machine
|
||||||
|
|
||||||
Het package is privé, dus ook een **pull** vereist een login — anoniem geeft
|
De organisatie is public, dus een **pull** werkt anoniem — `docker compose pull &&
|
||||||
`401 Unauthorized` (`docker compose pull` meldt dan "no basic auth credentials"
|
docker compose up -d` en klaar, zonder `docker login`.
|
||||||
of "pull access denied"). Eenmalig op de Docker-host:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker login vibehoogie.duckdns.org
|
|
||||||
# gebruikersnaam: je Gitea-account
|
|
||||||
# wachtwoord: access token met scope read:package
|
|
||||||
```
|
|
||||||
|
|
||||||
Daarna werkt `docker compose pull && docker compose up -d`. De login komt in
|
|
||||||
`~/.docker/config.json` van de gebruiker die het commando uitvoert — doe het dus
|
|
||||||
als dezelfde gebruiker (bij `sudo docker` is dat root). Log in op
|
|
||||||
`vibehoogie.duckdns.org` **zonder** `:3000`.
|
|
||||||
|
|
||||||
Controleren zonder te pullen:
|
Controleren zonder te pullen:
|
||||||
|
|
||||||
@@ -364,6 +470,24 @@ Controleren zonder te pullen:
|
|||||||
docker manifest inspect vibehoogie.duckdns.org/vibe/vibefinance-app:latest
|
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
|
||||||
@@ -398,16 +522,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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+75
-1
@@ -1,6 +1,80 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Niet vrijgegeven]
|
## [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
|
### Gewijzigd
|
||||||
- `release.ps1` leest een `.env.release` naast het script in (niet in git), zodat
|
- `release.ps1` leest een `.env.release` naast het script in (niet in git), zodat
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
|
|
||||||
Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer
|
Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
<img src="docs/screenshot-dashboard.png" alt="VibeFinance dashboard" width="820">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -46,9 +48,18 @@ openssl rand -hex 64
|
|||||||
|
|
||||||
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
De `Vibe`-organisatie in Gitea is publiek, dus een pull werkt anoniem — geen
|
||||||
|
`docker login` nodig. Krijg je toch `401 Unauthorized` of "pull access denied",
|
||||||
|
dan staat de organisatie weer op privé:
|
||||||
|
|
||||||
|
```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,7 +120,9 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -128,6 +141,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) |
|
| 📈 **Voortgang** | 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) |
|
||||||
|
|
||||||
|
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
|
## 🏗️ Architectuur
|
||||||
@@ -147,14 +164,18 @@ vibefinance/
|
|||||||
├── docker/
|
├── docker/
|
||||||
│ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container
|
│ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container
|
||||||
│
|
│
|
||||||
|
├── docs/
|
||||||
|
│ └── screenshot-dashboard.png # Screenshot bovenaan deze README
|
||||||
|
│
|
||||||
├── frontend/ # React / Vite SPA (build → dist)
|
├── frontend/ # React / Vite SPA (build → dist)
|
||||||
│ ├── vite.config.js # /api proxy naar de API in dezelfde container
|
│ ├── vite.config.js # /api proxy naar de API in dezelfde container
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── App.jsx # Root – wiring + idle-logout
|
│ ├── App.jsx # Root – wiring + idle-logout
|
||||||
│ ├── main.jsx
|
│ ├── main.jsx
|
||||||
|
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
|
||||||
│ ├── constants/ # Kleuren, tabs, begindata
|
│ ├── constants/ # Kleuren, tabs, begindata
|
||||||
│ ├── utils/ # Formatters, helpers
|
│ ├── utils/ # Formatters, helpers
|
||||||
│ ├── hooks/ # useTheme, useUpdateCheck
|
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
|
||||||
│ ├── context/ # AppContext (global state + API sync)
|
│ ├── context/ # AppContext (global state + API sync)
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
|
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
|
||||||
@@ -248,7 +269,7 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
|
|||||||
| `PORT` | `3001` | Poort binnen de container |
|
| `PORT` | `3001` | Poort binnen de container |
|
||||||
| `STATIC_DIR` | `/app/public` | Map met de frontend-build |
|
| `STATIC_DIR` | `/app/public` | Map met de frontend-build |
|
||||||
| `UPDATE_CHECK_URL` | Gitea releases-API | Bron update-melding (leeg = uit) |
|
| `UPDATE_CHECK_URL` | Gitea releases-API | Bron update-melding (leeg = uit) |
|
||||||
| `UPDATE_CHECK_TOKEN` | — | Alleen voor een niet-publieke repo |
|
| `UPDATE_CHECK_TOKEN` | — | Optioneel: de Gitea-repo is publiek, dus de check werkt anoniem. Alleen nodig als de organisatie weer privé wordt; scope `read:repository` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -277,7 +298,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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -68,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", () => {
|
||||||
|
|||||||
@@ -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 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vibefinance-frontend",
|
"name": "vibefinance-frontend",
|
||||||
"version": "0.1.8",
|
"version": "1.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vibefinance",
|
"name": "vibefinance",
|
||||||
"version": "0.1.9",
|
"version": "1.0.2-dev",
|
||||||
"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",
|
||||||
|
|||||||
+20
@@ -90,6 +90,26 @@ if ($frontendPkg.version -ne $appVersion) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Versiebadge in README.md ──────────────────────────────────────────────
|
||||||
|
# Anders loopt die badge stil achter: hij stond drie releases achter voordat
|
||||||
|
# dit erin zat, en het script kent het nummer toch al. Zelfde regel als
|
||||||
|
# hierboven: een droogloop meldt het, maar schrijft niet.
|
||||||
|
$readmePath = Join-Path $PSScriptRoot "README.md"
|
||||||
|
if (Test-Path $readmePath) {
|
||||||
|
$readme = [System.IO.File]::ReadAllText($readmePath)
|
||||||
|
$patroon = "badge/versie-[^-]+-"
|
||||||
|
$nieuw = "badge/versie-$appVersion-"
|
||||||
|
$huidig = [regex]::Match($readme, $patroon)
|
||||||
|
if ($huidig.Success -and $huidig.Value -ne $nieuw) {
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host "[DRY-RUN] Versiebadge in README.md: $($huidig.Value) -> $nieuw" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
[System.IO.File]::WriteAllText($readmePath, ($readme -replace $patroon, $nieuw))
|
||||||
|
Write-Host "Versiebadge in README.md bijgewerkt: $appVersion" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$gitTag = "$appVersion"
|
$gitTag = "$appVersion"
|
||||||
|
|
||||||
# ── Registry tags (één image voor frontend + backend) ─────────────────────
|
# ── Registry tags (één image voor frontend + backend) ─────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user