Compare commits
17
Commits
1b14553417
..
v1.0.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750dc69e56 | ||
|
|
b4d1e25e50 | ||
|
|
e315039c35 | ||
|
|
0d41c0af0b | ||
|
|
04e70208bd | ||
|
|
ceff734793 | ||
|
|
624f51679a | ||
|
|
5f90331694 | ||
|
|
4e923fa998 | ||
|
|
857322a490 | ||
|
|
6fd4f4766b | ||
|
|
e5f78f5368 | ||
|
|
bff64e5012 | ||
|
|
4e4a9ca3e1 | ||
|
|
00893fa6be | ||
|
|
b5a068d3d8 | ||
|
|
fdb9771916 |
@@ -96,59 +96,87 @@ docker compose -f docker-compose.dev.yml exec -w /app/frontend app npm run lint
|
|||||||
|
|
||||||
## Tokens
|
## Tokens
|
||||||
|
|
||||||
> **De `Vibe`-organisatie staat op public.** Alles wat alleen *leest* heeft
|
**Eén token, op het werkstation.** Dat is genoeg omdat de `Vibe`-organisatie op
|
||||||
> daardoor geen token meer nodig: `git clone`, `docker compose pull` en de
|
public staat: alles wat alleen *leest* heeft geen token meer nodig — `git clone`,
|
||||||
> update-check van de app werken anoniem. Het host-token hieronder is dus
|
`docker compose pull` en de update-check van de app werken anoniem. Wat overblijft
|
||||||
> optioneel geworden. *Schrijven* blijft geauthenticeerd — een publieke
|
is schrijven, en dat gebeurt allemaal vanaf deze ene machine.
|
||||||
> organisatie verandert daar niets aan.
|
|
||||||
|
|
||||||
Twee tokens, gesplitst op machine en op read/write. De grens die iets betekent
|
| Token | Machine | Scopes | Gebruikt voor |
|
||||||
ligt tussen werkstation en server, niet tussen push en release: op één machine
|
|---|---|---|---|
|
||||||
kan wie het ene token kan lezen, het andere ook.
|
| `vibefinance-workstation` | dit werkstation | `write:package`, `write:repository` | `docker push` (`release.ps1`, `dev.ps1 push`) én het aanmaken van de Gitea-release |
|
||||||
|
|
||||||
| Token | Machine | Scopes | Gebruikt voor | Nog nodig? |
|
Er is geen apart host-token en geen apart registry-token:
|
||||||
|---|---|---|---|---|
|
|
||||||
| `vibefinance-workstation` | dit werkstation | `write:package`, `write:repository` | `docker push` (`release.ps1`, `dev.ps1 push`) en het aanmaken van de Gitea-release | **Ja** |
|
|
||||||
| `vibefinance-host` | de Docker-host | `read:package`, `read:repository` | `docker compose pull` en de update-check van de app | Nee, zolang de organisatie public is |
|
|
||||||
|
|
||||||
Aanmaken: Gitea → Settings → Applications → Generate New Token. Doe dat onder een
|
- **`git push`** gaat over SSH (`gitea@…:Vibe/VibeFinance.git`), dus die gebruikt
|
||||||
account dat lid is van de `Vibe`-organisatie: voor schrijven volstaat
|
je SSH-sleutel en niet dit token. Werkt SSH even niet — bijvoorbeeld omdat de
|
||||||
organisatie-lidmaatschap niet automatisch, het account moet ook push-rechten op de
|
ssh-agent niet meedoet in een niet-interactieve shell, wat `Permission denied
|
||||||
repo hebben.
|
(publickey,password)` geeft — dan kun je *lezen* nu anoniem over https, omdat
|
||||||
|
de organisatie public is:
|
||||||
|
|
||||||
**Waar ze terechtkomen**
|
```powershell
|
||||||
|
git fetch https://vibehoogie.duckdns.org/Vibe/VibeFinance.git --prune --tags "+refs/heads/*:refs/remotes/origin/*"
|
||||||
|
```
|
||||||
|
|
||||||
| | Werkstation | Docker-host |
|
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`
|
||||||
| Registry | `docker login` (credential store), of `REGISTRY_USER` + `REGISTRY_TOKEN` in `.env.release` | niets nodig voor een pull; anders `docker login` als de gebruiker die deployt |
|
leest daarvoor `core.sshCommand` uit, omdat op Windows alleen de OpenSSH van het
|
||||||
| Gitea-API | `GITEA_TOKEN` in `.env.release` | niets nodig; optioneel `UPDATE_CHECK_TOKEN` in `.env` |
|
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.
|
||||||
|
|
||||||
**Waarom een host-token read-only blijft** — voor het geval de organisatie weer
|
Aanmaken: Gitea → Settings → Applications → Generate New Token, met **beide**
|
||||||
privé gaat en je er alsnog één neerzet. Dat staat in platte tekst in `.env` op
|
scopes aangevinkt. Doe dat onder een account dat push-rechten op de repo heeft —
|
||||||
een server die de app op poort 3400 publiceert. Met schrijfrechten kan wie bij dat
|
lid zijn van de organisatie is daarvoor niet automatisch genoeg. Scopes van een
|
||||||
bestand komt — via de container, een shell op die host, of een backup — een eigen
|
bestaand token zijn later niet aan te passen, dus vink ze in één keer goed aan.
|
||||||
image als `latest` naar de registry pushen, en die host pullt `latest` zelf.
|
|
||||||
Read-only houdt je eigen deploy-pad uit de aanvalsketen.
|
**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
|
### Wat een foutcode betekent
|
||||||
|
|
||||||
| Status | Betekenis |
|
| Status | Betekenis |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 401 | Token ongeldig of verlopen. Op de registry: helemaal niet ingelogd. |
|
| 401 | Token ongeldig, verlopen of ingetrokken — Gitea herkent hem niet. 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. |
|
| 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. |
|
| 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
|
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.
|
je dus een nieuw token aan; bijwerken kan niet.
|
||||||
|
|
||||||
Nakijken wie een token is en wat hij ziet:
|
**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
|
```powershell
|
||||||
$h = @{ Authorization = "token $env:GITEA_TOKEN" }
|
$u = "https://vibehoogie.duckdns.org/api/v1/user/repos"
|
||||||
(Invoke-RestMethod https://vibehoogie.duckdns.org/api/v1/user -Headers $h).login
|
try { (Invoke-WebRequest $u -Headers @{ Authorization = "token $env:GITEA_TOKEN" } -UseBasicParsing).StatusCode }
|
||||||
(Invoke-RestMethod https://vibehoogie.duckdns.org/api/v1/user/repos -Headers $h).full_name
|
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 —
|
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:
|
busybox-`wget` gooit die bij een foutstatus weg, en Node zit in het image:
|
||||||
|
|
||||||
@@ -168,14 +196,21 @@ docker exec vibefinance_app node -e 'const t=process.env.UPDATE_CHECK_TOKEN; fet
|
|||||||
> waarde helemaal geen header. Nagemeten op 2026-09-10:
|
> waarde helemaal geen header. Nagemeten op 2026-09-10:
|
||||||
> geen header `200`, `token ongeldig123` `401`, leeg `200`.
|
> geen header `200`, `token ongeldig123` `401`, leeg `200`.
|
||||||
|
|
||||||
### Tokens opruimen
|
### Oude tokens opruimen
|
||||||
|
|
||||||
De lijst onder Settings → Applications heeft een kolom **Last used**. Zet eerst
|
Nu er één token overblijft, kunnen de rest weg — maar niet op de gok. De lijst
|
||||||
beide machines om, draai daarna een release en een pull, en kijk een dag later
|
onder Settings → Applications heeft een kolom **Last used**. Werkwijze:
|
||||||
welke tokens niet meer meebewegen — die kun je intrekken. Betrouwbaarder dan uit
|
|
||||||
de naam afleiden wat waar in gebruik is. Trek niets in vóór die controle: het
|
1. Zet het nieuwe token in `.env.release` en draai een release die helemaal
|
||||||
token achter je `docker login` op dit werkstation laat een release op de push
|
doorloopt (inclusief `docker push` en stap 5).
|
||||||
falen.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -183,7 +218,8 @@ falen.
|
|||||||
|
|
||||||
```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: kop van "Niet vrijgegeven" naar "[x.x.x] - datum"
|
# 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:
|
# 3. Token staat in .env.release (zie Tokens) — of eenmalig in de shell:
|
||||||
$env:GITEA_TOKEN = "<vibefinance-workstation>"
|
$env:GITEA_TOKEN = "<vibefinance-workstation>"
|
||||||
|
|
||||||
@@ -191,8 +227,9 @@ $env:GITEA_TOKEN = "<vibefinance-workstation>"
|
|||||||
.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren
|
.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren
|
||||||
.\release.ps1 -NoBuild # sla docker build over, alleen tag + release
|
.\release.ps1 -NoBuild # sla docker build over, alleen tag + release
|
||||||
.\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
|
.\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||||
|
.\release.ps1 -NoChangelog # publiceer bewust zonder release-notes
|
||||||
|
|
||||||
# 4. Versienummer in package.json naar "x.x.(x+1)-dev" voor volgende cyclus
|
# Klaar. Het versienummer voor de volgende cyclus zet het script zelf.
|
||||||
```
|
```
|
||||||
|
|
||||||
Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde
|
Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde
|
||||||
@@ -200,38 +237,53 @@ tag op een Raspberry Pi en op een x86-server werkt.
|
|||||||
|
|
||||||
**Wat het script zelf bijwerkt** — met de hand aanpassen is dus dubbel werk:
|
**Wat het script zelf bijwerkt** — met de hand aanpassen is dus dubbel werk:
|
||||||
|
|
||||||
| Bestand | Wat |
|
| Bestand | Wat | Wanneer |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `frontend/package.json` | versie gelijkgetrokken met de root `package.json` |
|
| `Changelog.md` | kop `## [Niet vrijgegeven]` → `## [x.x.x] - datum` | vóór de build |
|
||||||
| `README.md` | de `versie-x.x.x`-badge bovenaan |
|
| Git-tag | `vx.x.x` op `HEAD`, gepusht naar origin | stap 4 |
|
||||||
|
| `frontend/package.json` | versie gelijkgetrokken met de root `package.json` | vóór de build |
|
||||||
|
| `README.md` | de `versie-x.x.x`-badge bovenaan | vóór de build |
|
||||||
|
| `package.json` (root) | versie → `x.x.(x+1)-dev` voor de volgende cyclus | **ná** het publiceren |
|
||||||
|
|
||||||
Beide gebeuren vóór de build, en een `-DryRun` meldt ze zonder te schrijven. Ze
|
Een `-DryRun` meldt de vier bestandswijzigingen zonder ze te schrijven (de
|
||||||
blijven na een echte release als wijziging in je working tree staan; neem ze mee
|
Git-tag is de uitzondering in die tabel: die hoort bij stap 4, niet bij de
|
||||||
in de volgende commit.
|
auto-updates). Na een echte release blijven die vier als wijziging in je working
|
||||||
|
tree staan; neem ze mee in de volgende commit — de tag staat dan op de commit
|
||||||
|
ervóór, dus de release bevat ze nog niet.
|
||||||
|
|
||||||
> **De changelog-kop moet exact `## [x.x.x]` zijn.** `release.ps1` zoekt die kop
|
De laatste staat bewust ná het publiceren: breekt de release eerder af, dan
|
||||||
> om de release-notes te vullen. Staat je werk nog onder `## [Niet vrijgegeven]`,
|
schuift het versienummer niet vooruit. Bijeffect dat je moet kennen — na een
|
||||||
> dan vindt het script niets en valt het terug op de tekst `"Release x.x.x"` —
|
geslaagde release staat er een `-dev` nummer in `package.json`, en de controle
|
||||||
> dan staat er geen inhoudelijke release-notes in Gitea en ziet een gebruiker in
|
bovenin het script weigert een `-dev` versie. Dat sluit een dubbele publicatie
|
||||||
> het belletje een placeholder. Dat overkwam 0.1.9. Achteraf repareren kan met
|
uit, maar wil je dezelfde versie bewust opnieuw releasen (bijvoorbeeld omdat een
|
||||||
> een `PATCH` op `/repos/<org>/<repo>/releases/<id>`, maar vóór de release
|
tag is verdwenen), zet het nummer dan eerst zelf terug naar `x.x.x`.
|
||||||
> omzetten is een stuk minder werk.
|
|
||||||
|
> **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
|
### `.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, en hoeft stap 3 niet meer met de hand:
|
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
|
# .env.release — staat in .gitignore, hoort alleen op de release-machine
|
||||||
GITEA_TOKEN=<vibefinance-workstation>
|
GITEA_TOKEN=<vibefinance-workstation>
|
||||||
REGISTRY_USER=<gitea-gebruiker>
|
REGISTRY_USER=<gitea-gebruiker>
|
||||||
REGISTRY_TOKEN=<vibefinance-workstation>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Beide tokenvariabelen krijgen hetzelfde token; zie [Tokens](#tokens) voor waarom
|
`REGISTRY_TOKEN` hoeft er niet in: beide scripts vallen bij een ontbrekende
|
||||||
dat op deze machine één token mag zijn, en voor wat een 401, 403 of 404 van Gitea
|
`REGISTRY_TOKEN` terug op `GITEA_TOKEN`. Zie [Tokens](#tokens) voor de scopes en
|
||||||
betekent.
|
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
|
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,
|
het bestand aan te passen. Het script logt alleen welke namen het heeft geladen,
|
||||||
@@ -249,7 +301,12 @@ docker compose pull && docker compose up -d
|
|||||||
docker compose down -v && docker compose up -d
|
docker compose down -v && docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Pullen vereist een registry-login op die host — zie
|
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).
|
[Pullen op een deploy-machine](#pullen-op-een-deploy-machine).
|
||||||
|
|
||||||
### PostgreSQL versie-upgrade
|
### PostgreSQL versie-upgrade
|
||||||
@@ -278,9 +335,11 @@ De Express-app serveert zowel de API als de gebouwde frontend:
|
|||||||
|
|
||||||
| Verzoek | Wat de app doet |
|
| Verzoek | Wat de app doet |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/api/...` | REST API (rate limited) |
|
| `/api/...` | REST API (rate limited, `Cache-Control: no-store`) |
|
||||||
| `/assets/...` | statische build uit `STATIC_DIR`, 1 jaar `immutable` gecached |
|
| `/assets/...` | gehashte build-bestanden uit `STATIC_DIR`, 1 jaar `immutable` |
|
||||||
| al het overige | `index.html` — fallback voor React-routing |
|
| `/favicon.svg`, `/icons/...` | vaste namen, dus `max-age=0, must-revalidate` |
|
||||||
|
| pad mét extensie zonder bestand | echte 404, niet `index.html` |
|
||||||
|
| al het overige | `index.html` — fallback voor React-routing, `no-store` |
|
||||||
|
|
||||||
`STATIC_DIR` staat standaard op `/app/public`; daar zet de `Dockerfile` de
|
`STATIC_DIR` staat standaard op `/app/public`; daar zet de `Dockerfile` de
|
||||||
Vite-build neer. Bestaat die map niet (bijv. in dev), dan serveert de app
|
Vite-build neer. Bestaat die map niet (bijv. in dev), dan serveert de app
|
||||||
@@ -326,6 +385,12 @@ De backend slaat alle data op in **PostgreSQL**.
|
|||||||
|
|
||||||
> PostgreSQL 18 gebruikt `PGDATA=/var/lib/postgresql/18/docker`. Het volume is gemount op `/var/lib/postgresql` zodat data bewaard blijft na een container restart.
|
> PostgreSQL 18 gebruikt `PGDATA=/var/lib/postgresql/18/docker`. Het volume is gemount op `/var/lib/postgresql` zodat data bewaard blijft na een container restart.
|
||||||
|
|
||||||
|
Daarnaast heeft de app zelf een backup onder **Instellingen**: die schrijft een
|
||||||
|
ZIP met de data van de *ingelogde* gebruiker (`data.json` + `meta.json`) en leest
|
||||||
|
zo'n ZIP ook weer in. Handig per gebruiker, maar het is geen servercopie — accounts,
|
||||||
|
wachtwoordhashes en de data van andere gebruikers zitten er niet in. Voor een
|
||||||
|
volledige backup gebruik je `pg_dump` hieronder.
|
||||||
|
|
||||||
**Backup maken:**
|
**Backup maken:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -352,12 +417,25 @@ docker exec -i vibefinance_postgres psql -U vibefinance -d vibefinance < backup-
|
|||||||
|
|
||||||
De eerste geregistreerde gebruiker krijgt automatisch de **Admin**-rol. Verdere accounts kunnen via de app aangemaakt worden (Gebruikersbeheer, Admin vereist) of via de API.
|
De eerste geregistreerde gebruiker krijgt automatisch de **Admin**-rol. Verdere accounts kunnen via de app aangemaakt worden (Gebruikersbeheer, Admin vereist) of via de API.
|
||||||
|
|
||||||
Rollen:
|
### Rollen
|
||||||
| Rol | Rechten |
|
|
||||||
|
De rolkeuze in Gebruikersbeheer biedt drie waarden, maar de backend dwingt er op
|
||||||
|
dit moment één af:
|
||||||
|
|
||||||
|
| Rol | Wat de code ermee doet |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Admin` | Alles, inclusief gebruikersbeheer |
|
| `Admin` | Enige rol die wordt gecontroleerd: `requireAdmin` in `middleware/auth.js` bewaakt alle routes onder `/api/users` |
|
||||||
| `Bewerker` | Data lezen en bewerken |
|
| `Bewerker` | Geen eigen rechten — gelijk aan `Viewer` |
|
||||||
| `Viewer` | Alleen lezen |
|
| `Viewer` | Standaardrol voor een nieuw account; mag de eigen data lezen én schrijven |
|
||||||
|
|
||||||
|
Dat `Viewer` toch mag schrijven is geen gat in de afscherming: `/api/data` werkt
|
||||||
|
per gebruiker en gaat altijd over de rij van het eigen token (`req.user.sub`).
|
||||||
|
Niemand ziet dus data van een ander. Wat ontbreekt is een read-only account —
|
||||||
|
wil je dat, dan is er een controle op `rol` nodig bij `PUT`/`DELETE /api/data`.
|
||||||
|
|
||||||
|
> De "Gebruikers"-knop in de navigatiebalk staat bij iedereen in beeld; bij een
|
||||||
|
> niet-Admin antwoordt `/api/users` met `403` en blijft de lijst leeg. De
|
||||||
|
> afscherming zit dus in de API, niet in de navigatie.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -419,6 +497,10 @@ Tags:
|
|||||||
x.x.x-dev development — specifieke dev versie
|
x.x.x-dev development — specifieke dev versie
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Image-tags hebben géén `v`-prefix (`1.0.4`), Git-tags wél (`v1.0.4`). Dat is
|
||||||
|
> geen inconsistentie maar de conventie aan beide kanten: container-registries
|
||||||
|
> taggen op het kale nummer, Git-releases met `v`.
|
||||||
|
|
||||||
Inloggen op de registry:
|
Inloggen op de registry:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -430,8 +512,9 @@ machine — `vibefinance-workstation`, zie [Tokens](#tokens). Zonder login faalt
|
|||||||
een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push` controleren dit
|
een push met `401 Unauthorized`; `release.ps1` en `dev.ps1 push` controleren dit
|
||||||
vóór de build en stoppen dan meteen.
|
vóór de build en stoppen dan meteen.
|
||||||
|
|
||||||
Liever niet handmatig inloggen? Zet `REGISTRY_USER` en `REGISTRY_TOKEN` in
|
Liever niet handmatig inloggen? Zet `REGISTRY_USER` in `.env.release`, dan logt
|
||||||
`.env.release`, dan logt het script zelf in.
|
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
|
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
|
||||||
@@ -489,7 +572,43 @@ dat op het belletje in de topbalk en onder "Over VibeFinance".
|
|||||||
|
|
||||||
Builds met een `-dev` suffix melden nooit (die lopen voor op de laatste release).
|
Builds met een `-dev` suffix melden nooit (die lopen voor op de laatste release).
|
||||||
Wil je de melding tijdens ontwikkeling toch testen: open de app met `?updatecheck`
|
Wil je de melding tijdens ontwikkeling toch testen: open de app met `?updatecheck`
|
||||||
in de URL.
|
in de URL. Let op dat `isNieuwer` alleen bij een écht hoger nummer aanslaat — bij
|
||||||
|
`1.0.2-dev` naast release `1.0.1` blijft de kaart dus ook met `?updatecheck` leeg.
|
||||||
|
Zet dan tijdelijk een lagere versie in de root `package.json`.
|
||||||
|
|
||||||
|
**Wat de melding toont**
|
||||||
|
|
||||||
|
Alleen de kop, de versieregel en een link:
|
||||||
|
|
||||||
|
```
|
||||||
|
🔔 Nieuwe versie beschikbaar
|
||||||
|
v1.0.1 is uitgebracht. Je gebruikt momenteel v1.0.0.
|
||||||
|
Release bekijken →
|
||||||
|
```
|
||||||
|
|
||||||
|
De release-notes staan er bewust **niet** in. `/api/update-check` stuurt ze wel
|
||||||
|
mee als `notes`, maar dat is de ruwe Markdown uit de changelog; in een kaart van
|
||||||
|
280px leverde dat zichtbare `### `-koppen en afgekapte regels op. Wie de details
|
||||||
|
wil, volgt de link. Het veld blijft in de API zitten voor het geval je ze ooit
|
||||||
|
ergens met meer ruimte wilt tonen — dan is Markdown renderen de volgende stap.
|
||||||
|
|
||||||
|
**De release-link hangt aan Gitea's `ROOT_URL`**
|
||||||
|
|
||||||
|
De `url` in het antwoord is de `html_url` van de release, en die stelt Gitea bij
|
||||||
|
elk verzoek samen uit `ROOT_URL` in `app.ini` — hij wordt *niet* per release
|
||||||
|
opgeslagen. Staat `ROOT_URL` op een intern adres, dan wijst "Release bekijken"
|
||||||
|
naar iets dat buiten je netwerk niet bestaat, terwijl de melding zelf gewoon
|
||||||
|
werkt (die leest alleen het versienummer).
|
||||||
|
|
||||||
|
Nakijken welke waarde Gitea daadwerkelijk gebruikt — de `issuer` is `ROOT_URL`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s https://vibehoogie.duckdns.org/.well-known/openid-configuration | grep issuer
|
||||||
|
```
|
||||||
|
|
||||||
|
`app.ini` wordt alleen bij het opstarten gelezen, dus na een wijziging moet Gitea
|
||||||
|
herstarten. Omdat de URL wordt samengesteld en niet bewaard, zijn daarna álle
|
||||||
|
releases in één keer goed — ook de oude. Een `PATCH` per release is niet nodig.
|
||||||
|
|
||||||
**Configuratie**
|
**Configuratie**
|
||||||
|
|
||||||
|
|||||||
+148
@@ -2,6 +2,149 @@
|
|||||||
|
|
||||||
## [Niet vrijgegeven]
|
## [Niet vrijgegeven]
|
||||||
|
|
||||||
|
### Opgelost
|
||||||
|
- De tooltip van de dashboardgrafiek viel buiten de tekening zodra vermogen en
|
||||||
|
inleg ver uit elkaar lagen. Hij werd boven het hoogste punt geplaatst en klapte
|
||||||
|
naar onderen als daar geen ruimte was — maar dat "onderen" rekende vanaf het
|
||||||
|
laagste punt, en met een kaart van 76px kwam hij dan onder het tekenvlak
|
||||||
|
terecht: over de datumlabels heen en half afgekapt. Precies in het midden van
|
||||||
|
de grafiek, waar je hem het meest gebruikt. De kaart staat nu náást de
|
||||||
|
hoverlijn (links of rechts, afhankelijk van waar de meeste ruimte is) en wordt
|
||||||
|
verticaal geklemd binnen het tekenvlak, dus hij past altijd
|
||||||
|
|
||||||
|
## [1.0.6] - 2026-09-11
|
||||||
|
|
||||||
|
### Toegevoegd
|
||||||
|
- De grafiek "Ontwikkeling van Eigen Vermogen" op het dashboard heeft een tweede
|
||||||
|
lijn: de **cumulatief geïnvesteerde fiat**. Die is opgebouwd uit dezelfde
|
||||||
|
gegevens als de KPI's erboven — het oudste voortgangsmoment telt als inleg,
|
||||||
|
elke latere rij telt zijn stortingen erbij op — zodat het laatste punt van de
|
||||||
|
lijn exact gelijk is aan "Totaal geïnvesteerde fiat". Het verschil tussen de
|
||||||
|
twee lijnen is je rendement, en dat staat nu ook in de tooltip: vermogen,
|
||||||
|
ingelegd en resultaat per datum
|
||||||
|
- Legenda boven de grafiek, nodig zodra er meer dan één lijn in staat
|
||||||
|
|
||||||
|
### Gewijzigd
|
||||||
|
- De inleglijn wordt met rechte segmenten getekend en niet met de
|
||||||
|
Catmull-Rom-afronding van de vermogenslijn. Cumulatieve inleg loopt nooit
|
||||||
|
terug, maar de bezier-afronding trekt tussen twee ongelijke stortingen een
|
||||||
|
doorbuiging onder het vorige punt — dan zou de grafiek een daling tonen die
|
||||||
|
in de data niet bestaat
|
||||||
|
- De y-as schaalt op beide reeksen. Op alleen de vermogenswaarden zou de
|
||||||
|
inleglijn buiten beeld lopen zodra je meer hebt ingelegd dan het vermogen
|
||||||
|
waard is
|
||||||
|
- De schermentabel in `README.md` beschreef het Dashboard als "totaal eigen
|
||||||
|
vermogen, schulden en netto vermogen". Dat klopte niet: `DashboardTab.jsx`
|
||||||
|
leest `totSchuld` helemaal niet en toont geen schuld en geen nettowaarde — dat
|
||||||
|
staat op het Voortgang-tabblad. De rij noemt nu wat er wél staat (KPI's over
|
||||||
|
inleg, vermogen, winst en ROI, de doelbalk en de grafiek), met een alinea
|
||||||
|
eronder over de twee lijnen
|
||||||
|
|
||||||
|
### Bekend
|
||||||
|
- De screenshot bovenaan `README.md` is van vóór deze wijziging en toont de
|
||||||
|
grafiek nog met één lijn
|
||||||
|
|
||||||
|
## [1.0.5] - 2026-09-11
|
||||||
|
|
||||||
|
### Gewijzigd
|
||||||
|
- De documentatie is gelijkgetrokken met de code. `README.md`: de bestandsboom
|
||||||
|
miste `backend/src/logger.js`, `docker/check-updates.mjs`,
|
||||||
|
`frontend/public/`, `InstellingenModal.jsx`, `LoginBackground.jsx` en de
|
||||||
|
losse bestanden in `utils/`, en noemde de update-route `update-check` terwijl
|
||||||
|
het bestand `routes/update.js` heet. De schermentabel gebruikt nu de labels
|
||||||
|
zoals ze in `constants/index.js` staan ("Schuld", "Voortgang Schuld & EV") en
|
||||||
|
vermeldt **Instellingen** — thema plus backup/restore via ZIP, dat nergens in
|
||||||
|
de README stond
|
||||||
|
- De tabel "wat de app met een verzoek doet" (in beide bestanden) noemde alleen
|
||||||
|
`/assets/...` en de SPA-fallback. `server.js` kent vier gevallen: `no-store`
|
||||||
|
op de API en op HTML, een jaar `immutable` op de gehashte bundels,
|
||||||
|
`must-revalidate` op bestanden met een vaste naam (`favicon.svg`, `icons/*`)
|
||||||
|
en een echte 404 op een pad mét extensie waar geen bestand achter zit
|
||||||
|
- `BEHEER.md` beschreef `Bewerker` als "data lezen en bewerken" en `Viewer` als
|
||||||
|
"alleen lezen". Zo werkt het niet: `requireAdmin` is de enige rolcontrole in
|
||||||
|
de code en bewaakt alleen `/api/users`. Een `Viewer` mag daardoor gewoon zijn
|
||||||
|
eigen data schrijven — geen gat in de afscherming, want `/api/data` gaat altijd
|
||||||
|
over de rij van het eigen token, maar wél iets anders dan er stond. De sectie
|
||||||
|
zegt nu wat de code doet en wat er nodig is voor een echt read-only account
|
||||||
|
- `README.md` en `BEHEER.md` wijzen bij backup naar het juiste niveau: de ZIP
|
||||||
|
onder Instellingen bevat alleen de data van de ingelogde gebruiker, `pg_dump`
|
||||||
|
is de volledige servercopie
|
||||||
|
- `DATABASE_URL` staat in de variabelentabel van `README.md`. Hij wordt door
|
||||||
|
`docker-compose.yml` samengesteld uit de `POSTGRES_*` waarden, dus zelf zetten
|
||||||
|
is alleen nodig buiten Compose
|
||||||
|
|
||||||
|
## [1.0.4] - 2026-09-10
|
||||||
|
|
||||||
|
### Gewijzigd
|
||||||
|
- Git-tags krijgen weer een `v`-prefix (`v1.0.4`), net als t/m `v1.0.0`. Sinds
|
||||||
|
1.0.1 was dat `1.0.1`, `1.0.2`, `1.0.3` — en omdat Gitea de tag op de
|
||||||
|
release-pagina toont, stond daar het kale nummer terwijl de release-naam wél
|
||||||
|
`v1.0.3` was. De image-tags in de registry houden bewust géén `v`; dat is de
|
||||||
|
conventie voor container-tags. De update-check verdraagt beide: backend en
|
||||||
|
frontend strippen een leidende `v` voordat ze versies vergelijken
|
||||||
|
|
||||||
|
### Opgelost
|
||||||
|
- Het omzetten van de changelog-kop slokte de lege regel eronder op, waardoor
|
||||||
|
`## [x.x.x] - datum` direct tegen `### Toegevoegd` aan kwam te staan. Oorzaak
|
||||||
|
was `\s*$` in het zoekpatroon: `\s` matcht ook newlines. Het patroon eindigt nu
|
||||||
|
op `[ \t]*(?=\r?$)` — een lookahead, want dit bestand heeft CRLF en met alleen
|
||||||
|
`[ \t]*$` matchte de kop helemaal niet meer (de `\r` zit ertussen). Zo blijft
|
||||||
|
de match binnen één regel én blijft het regeleinde ongemoeid
|
||||||
|
|
||||||
|
## [1.0.3] - 2026-09-10
|
||||||
|
|
||||||
|
### Toegevoegd
|
||||||
|
- `release.ps1` zet de changelog-kop `## [Niet vrijgegeven]` nu zelf om naar
|
||||||
|
`## [x.x.x] - datum`, net zoals het al deed met `frontend/package.json` en de
|
||||||
|
README-badge. Handmatig omzetten hoeft dus niet meer
|
||||||
|
- Staat er ook na die omzetting niets over deze versie in `Changelog.md`, dan
|
||||||
|
breekt het script af **vóór** de build met de melding wat je moet toevoegen.
|
||||||
|
Eerder viel het stil terug op de tekst `"Release x.x.x"` en meldde het
|
||||||
|
"Gepubliceerd" — dat overkwam 0.1.9 en 1.0.2, en is achteraf alleen met een
|
||||||
|
`PATCH` op de release te herstellen. Nieuwe switch `-NoChangelog` om bewust
|
||||||
|
zonder release-notes te publiceren
|
||||||
|
- `release.ps1` zet na een geslaagde release de versie in de root
|
||||||
|
`package.json` op `x.x.(x+1)-dev`. Dat was voorheen de laatste handmatige stap
|
||||||
|
van de release-workflow. Het staat bewust ná het publiceren, zodat een
|
||||||
|
afgebroken release het nummer niet vooruit schuift
|
||||||
|
|
||||||
|
### Opgelost
|
||||||
|
- **`-DryRun` in combinatie met `-Force` verwijderde écht de Git-tag.** De twee
|
||||||
|
commando's in stap 4 (`git tag -d` en `git push origin :refs/tags/x.x.x`)
|
||||||
|
liepen niet via `Invoke-Cmd` en hadden dus geen droogloop-controle. Gitea
|
||||||
|
ruimt de release op zodra de tag verdwijnt, dus een droogloop kon een
|
||||||
|
gepubliceerde release wissen — zo is de release van 1.0.2 verloren gegaan en
|
||||||
|
opnieuw aangemaakt. Onder `-DryRun` worden beide nu alleen gemeld
|
||||||
|
|
||||||
|
### Gewijzigd
|
||||||
|
- De changelog-controle staat vóór de drie auto-updates, zodat een afgebroken
|
||||||
|
release geen gewijzigde bestanden achterlaat terwijl hij "er is niets
|
||||||
|
gepubliceerd" meldt
|
||||||
|
- Stap 5 leest de release-notes uit het geheugen in plaats van opnieuw van
|
||||||
|
schijf. Een droogloop schrijft de kop niet, en toonde daardoor de placeholder
|
||||||
|
in de payload — precies wat je met een droogloop wilt uitsluiten
|
||||||
|
- De Quick Start in `README.md` begint nu met het ophalen van
|
||||||
|
`docker-compose.yml` (clone, of alleen die twee bestanden via `raw`). Zonder
|
||||||
|
dat bestand antwoordt `docker compose pull` met `no configuration file
|
||||||
|
provided: not found`, en die stap ontbrak. Beide foutmeldingen staan er nu bij
|
||||||
|
met hun oorzaak
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-09-10
|
||||||
|
|
||||||
|
### Gewijzigd
|
||||||
|
- De update-melding op het belletje toont alleen nog de kop, de versieregel en de
|
||||||
|
link naar de release. De release-notes stonden er als ruwe Markdown in — in een
|
||||||
|
kaart van 280px leverde dat zichtbare `### `-koppen en afgekapte regels op.
|
||||||
|
`/api/update-check` stuurt `notes` nog wel mee, zodat ze later ergens met meer
|
||||||
|
ruimte gebruikt kunnen worden
|
||||||
|
- `BEHEER.md` legt vast dat de release-link uit Gitea's `ROOT_URL` wordt
|
||||||
|
samengesteld en niet per release wordt opgeslagen: na een `ROOT_URL`-wijziging
|
||||||
|
plus herstart zijn álle releases in één keer goed, ook de oude. Verder staat er
|
||||||
|
nu hoe je aan het verschil tussen `401` en `403` ziet of een token nog leeft, en
|
||||||
|
hoe je anoniem over https fetcht als SSH niet meedoet
|
||||||
|
|
||||||
|
## [1.0.1] - 2026-09-10
|
||||||
|
|
||||||
### Toegevoegd
|
### Toegevoegd
|
||||||
- Kolommen in de tabellen op Eigen Vermogen ("Geïnvesteerde Fiat") en Voortgang
|
- Kolommen in de tabellen op Eigen Vermogen ("Geïnvesteerde Fiat") en Voortgang
|
||||||
zijn versleepbaar: pak de rechterrand van een kolomkop. De breedtes gaan per
|
zijn versleepbaar: pak de rechterrand van een kolomkop. De breedtes gaan per
|
||||||
@@ -17,6 +160,11 @@
|
|||||||
ondersteunt hem nog als vangnet voor het geval de organisatie weer privé
|
ondersteunt hem nog als vangnet voor het geval de organisatie weer privé
|
||||||
wordt. Schrijven verandert niet: `docker push` en het aanmaken van een release
|
wordt. Schrijven verandert niet: `docker push` en het aanmaken van een release
|
||||||
hebben nog steeds het werkstation-token nodig
|
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`,
|
- De tabellen op Eigen Vermogen en Voortgang staan op `table-layout: fixed`,
|
||||||
nodig om ingestelde kolombreedtes te respecteren. Gevolg: lange tekst rekt een
|
nodig om ingestelde kolombreedtes te respecteren. Gevolg: lange tekst rekt een
|
||||||
kolom niet meer op maar wordt afgekapt met een ellips. De Opmerkingen-cellen
|
kolom niet meer op maar wordt afgekapt met een ellips. De Opmerkingen-cellen
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
<img src="docs/screenshot-dashboard.png" alt="VibeFinance dashboard" width="820">
|
<img src="docs/screenshot-dashboard.png" alt="VibeFinance dashboard" width="820">
|
||||||
@@ -32,7 +32,25 @@ Deze applicatie is volledig ontworpen en ontwikkeld met behulp van [Claude.ai](h
|
|||||||
De app draait in **2 containers**: `app` (Express API + gebouwde frontend) en
|
De app draait in **2 containers**: `app` (Express API + gebouwde frontend) en
|
||||||
`postgres` (database).
|
`postgres` (database).
|
||||||
|
|
||||||
**`.env`** — kopieer `.env.example` en vul in:
|
**1. De bestanden op de server zetten.** `docker compose` leest
|
||||||
|
`docker-compose.yml` uit de map waarin je het commando draait, dus dat bestand
|
||||||
|
moet er eerst staan. De organisatie in Gitea is publiek, dus dit kan anoniem:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://vibehoogie.duckdns.org/Vibe/VibeFinance.git
|
||||||
|
cd VibeFinance
|
||||||
|
```
|
||||||
|
|
||||||
|
Wil je de repo niet op je server, dan zijn deze twee bestanden genoeg:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p vibefinance && cd vibefinance
|
||||||
|
base=https://vibehoogie.duckdns.org/Vibe/VibeFinance/raw/branch/main
|
||||||
|
curl -fsSLO $base/docker-compose.yml
|
||||||
|
curl -fsSL $base/.env.example -o .env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. `.env` aanmaken** — kopieer `.env.example` en vul in:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
POSTGRES_USER=vibefinance
|
POSTGRES_USER=vibefinance
|
||||||
@@ -46,19 +64,25 @@ APP_PORT=3400
|
|||||||
openssl rand -hex 64
|
openssl rand -hex 64
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**3. Starten:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # en vul de waarden in
|
cp .env.example .env # en vul de waarden in
|
||||||
|
|
||||||
docker compose pull && docker compose up -d
|
docker compose pull && docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
De `Vibe`-organisatie in Gitea is publiek, dus een pull werkt anoniem — geen
|
Twee foutmeldingen en wat ze betekenen:
|
||||||
`docker login` nodig. Krijg je toch `401 Unauthorized` of "pull access denied",
|
|
||||||
dan staat de organisatie weer op privé:
|
|
||||||
|
|
||||||
```bash
|
- **`no configuration file provided: not found`** — je staat in een map zonder
|
||||||
docker login vibehoogie.duckdns.org # wachtwoord = Gitea-token met read:package
|
`docker-compose.yml`. Controleer met `ls docker-compose.yml`, of geef het
|
||||||
```
|
bestand expliciet mee: `docker compose -f /pad/naar/docker-compose.yml up -d`.
|
||||||
|
- **`401 Unauthorized`** of **"pull access denied"** — de organisatie staat niet
|
||||||
|
meer op publiek. Dan alsnog inloggen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login vibehoogie.duckdns.org # wachtwoord = Gitea-token met read:package
|
||||||
|
```
|
||||||
|
|
||||||
Navigeer naar `http://<server-ip>:3400` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol.
|
Navigeer naar `http://<server-ip>:3400` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol.
|
||||||
|
|
||||||
@@ -121,29 +145,51 @@ backend/package.json (15 dependencies)
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# token: staat in .env.release, of eenmalig in de shell
|
# token: staat in .env.release, of eenmalig in de shell
|
||||||
$env:GITEA_TOKEN = "<token met write:repository>"
|
# één Gitea-token met write:package (push) én write:repository (release)
|
||||||
|
$env:GITEA_TOKEN = "<token met write:package + write:repository>"
|
||||||
.\release.ps1
|
.\release.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Draait de tests, bouwt het app-image voor `amd64` + `arm64` (Raspberry Pi),
|
Draait de tests, bouwt het app-image voor `amd64` + `arm64` (Raspberry Pi),
|
||||||
pusht naar de registry, zet de Git-tag en publiceert de Gitea-release.
|
pusht naar de registry, zet de Git-tag en publiceert de Gitea-release.
|
||||||
|
|
||||||
|
Het script regelt ook het bijwerken eromheen: de changelog-kop
|
||||||
|
`## [Niet vrijgegeven]` wordt `## [x.x.x] - datum`, de versie in
|
||||||
|
`frontend/package.json` en de badge hierboven gaan mee, en ná het publiceren
|
||||||
|
zet het de root `package.json` op `x.x.(x+1)-dev` voor de volgende cyclus.
|
||||||
|
Beschrijft de changelog niets over deze versie, dan stopt het script vóór de
|
||||||
|
build — anders zou de release met een placeholder als notes online komen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🗂️ Functionaliteit
|
## 🗂️ Functionaliteit
|
||||||
|
|
||||||
| Scherm | Wat het doet |
|
| Scherm | Wat het doet |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 📊 **Dashboard** | Overzicht van totaal eigen vermogen, schulden en netto vermogen |
|
| 📊 **Dashboard** | KPI's over inleg, vermogen, winst en ROI, de voortgang naar je doel, en de grafiek met vermogen tegen inleg |
|
||||||
| 💰 **Eigen vermogen** | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek |
|
| 💰 **Eigen vermogen** | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek |
|
||||||
| 📉 **Schulden** | Schulden bijhouden per blok met afbetalingsoverzicht |
|
| 📉 **Schuld** | Schulden bijhouden per blok met afbetalingsoverzicht |
|
||||||
| 📈 **Voortgang** | Grafieken en trends van EV en schuld over tijd (apart bij te houden) |
|
| 📈 **Voortgang Schuld & EV** | Grafieken en trends van EV en schuld over tijd (apart bij te houden) |
|
||||||
| 👥 **Gebruikers** | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) |
|
| 👥 **Gebruikers** | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) |
|
||||||
|
| ⚙️ **Instellingen** | Licht/donker thema, backup downloaden en backup terugzetten |
|
||||||
|
|
||||||
|
De grafiek "Ontwikkeling van Eigen Vermogen" op het dashboard tekent twee lijnen
|
||||||
|
op één as: je netto waarde per voortgangsmoment, en daaronder de cumulatief
|
||||||
|
geïnvesteerde fiat. Het verschil tussen de twee is je rendement; de tooltip zet
|
||||||
|
vermogen, inleg en resultaat per datum onder elkaar. Het laatste punt van de
|
||||||
|
inleglijn is per definitie gelijk aan de KPI "Totaal geïnvesteerde fiat" — beide
|
||||||
|
rekenen het oudste voortgangsmoment als startinleg en tellen daar de stortingen
|
||||||
|
per rij bij op.
|
||||||
|
|
||||||
De kolommen in de tabellen op Eigen vermogen en Voortgang zijn versleepbaar: pak
|
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
|
de rechterrand van een kolomkop. De breedtes worden per tabel in de browser
|
||||||
bewaard; dubbelklik op de greep voor de standaardbreedte.
|
bewaard; dubbelklik op de greep voor de standaardbreedte.
|
||||||
|
|
||||||
|
Backup en restore lopen via **Instellingen** in de navigatiebalk: "Backup maken"
|
||||||
|
downloadt een ZIP (`data.json` + `meta.json`) met je eigen financiële data,
|
||||||
|
"Backup terugzetten" leest zo'n ZIP weer in en schrijft hem via `/api/data` weg.
|
||||||
|
Dat is de gebruikersvariant; een volledige database-dump staat in `BEHEER.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏗️ Architectuur
|
## 🏗️ Architectuur
|
||||||
@@ -161,26 +207,33 @@ vibefinance/
|
|||||||
├── release.ps1 / release.sh # Release (test, push, tag, Gitea-release)
|
├── release.ps1 / release.sh # Release (test, push, tag, Gitea-release)
|
||||||
│
|
│
|
||||||
├── docker/
|
├── docker/
|
||||||
│ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container
|
│ ├── dev-entrypoint.sh # Start API + Vite samen in de dev-container
|
||||||
|
│ ├── check-updates.mjs # Dependency-check tegen het npm-register
|
||||||
|
│ └── update-exceptions.json # Pakketten die bewust achterblijven, met reden
|
||||||
│
|
│
|
||||||
├── docs/
|
├── docs/
|
||||||
│ └── screenshot-dashboard.png # Screenshot bovenaan deze README
|
│ └── screenshot-dashboard.png # Screenshot bovenaan deze README
|
||||||
│
|
│
|
||||||
├── frontend/ # React / Vite SPA (build → dist)
|
├── frontend/ # React / Vite SPA (build → dist)
|
||||||
│ ├── vite.config.js # /api proxy naar de API in dezelfde container
|
│ ├── vite.config.js # /api proxy naar de API in dezelfde container
|
||||||
|
│ ├── index.html # Vite-entry, verwijst naar /favicon.svg
|
||||||
|
│ ├── public/ # Ongebundeld: favicon.svg + icons/
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── App.jsx # Root – wiring + idle-logout
|
│ ├── App.jsx # Root – wiring + idle-logout
|
||||||
|
│ ├── App.css # Reset + losse klassen (o.a. .kolom-grip)
|
||||||
│ ├── main.jsx
|
│ ├── main.jsx
|
||||||
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
|
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
|
||||||
│ ├── constants/ # Kleuren, tabs, begindata
|
│ ├── constants/ # Kleuren, tabs, begindata
|
||||||
│ ├── utils/ # Formatters, helpers
|
│ ├── utils/ # format.js (bedragen/datums), storage.js
|
||||||
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
|
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
|
||||||
│ ├── context/ # AppContext (global state + API sync)
|
│ ├── context/ # AppContext (global state + API sync + backup)
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
|
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
|
||||||
│ │ ├── LoginPage.jsx
|
│ │ ├── LoginPage.jsx
|
||||||
|
│ │ ├── LoginBackground.jsx
|
||||||
│ │ ├── NavBar.jsx
|
│ │ ├── NavBar.jsx
|
||||||
│ │ ├── ProfielPopup.jsx
|
│ │ ├── ProfielPopup.jsx
|
||||||
|
│ │ ├── InstellingenModal.jsx # Thema + backup/restore
|
||||||
│ │ └── GebruikersBeheer.jsx
|
│ │ └── GebruikersBeheer.jsx
|
||||||
│ └── pages/
|
│ └── pages/
|
||||||
│ ├── DashboardTab.jsx
|
│ ├── DashboardTab.jsx
|
||||||
@@ -193,10 +246,11 @@ vibefinance/
|
|||||||
└── src/
|
└── src/
|
||||||
├── server.js # Entry point: API op /api, SPA op /
|
├── server.js # Entry point: API op /api, SPA op /
|
||||||
├── db.js # PostgreSQL pool + initDb() schema-migraties
|
├── db.js # PostgreSQL pool + initDb() schema-migraties
|
||||||
|
├── logger.js # pino (pino-pretty buiten productie)
|
||||||
├── config/ # Omgevingsvariabelen (incl. STATIC_DIR)
|
├── config/ # Omgevingsvariabelen (incl. STATIC_DIR)
|
||||||
├── constants/ # Begindata (initialData.js)
|
├── constants/ # Begindata (initialData.js)
|
||||||
├── middleware/ # auth.js, errorHandler.js
|
├── middleware/ # auth.js, errorHandler.js
|
||||||
├── routes/ # auth, data, users, health, update-check
|
├── routes/ # auth, data, users, health, update
|
||||||
└── stores/ # userStore, dataStore
|
└── stores/ # userStore, dataStore
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -212,9 +266,11 @@ frontend en API.
|
|||||||
|
|
||||||
| Verzoek | Wat de app doet |
|
| Verzoek | Wat de app doet |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/api/...` | REST API (rate limited) |
|
| `/api/...` | REST API (rate limited, `Cache-Control: no-store`) |
|
||||||
| `/assets/...` | Statische build, 1 jaar `immutable` gecached |
|
| `/assets/...` | Gehashte build-bestanden, 1 jaar `immutable` gecached |
|
||||||
| al het overige | `index.html` — fallback voor React-routing |
|
| `/favicon.svg`, `/icons/...` | Vaste namen, dus `max-age=0, must-revalidate` |
|
||||||
|
| pad mét extensie, geen bestand | echte 404 (geen HTML waar een icoon wordt verwacht) |
|
||||||
|
| al het overige | `index.html` — fallback voor React-routing, niet gecached |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -262,6 +318,7 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
|
|||||||
| `JWT_SECRET` | — | **Verplicht** — minimaal 64 tekens |
|
| `JWT_SECRET` | — | **Verplicht** — minimaal 64 tekens |
|
||||||
| `POSTGRES_USER` | `vibefinance` | Database gebruikersnaam |
|
| `POSTGRES_USER` | `vibefinance` | Database gebruikersnaam |
|
||||||
| `POSTGRES_DB` | `vibefinance` | Database naam |
|
| `POSTGRES_DB` | `vibefinance` | Database naam |
|
||||||
|
| `DATABASE_URL` | — | Connectiestring; `docker-compose.yml` stelt hem samen uit de drie `POSTGRES_*` waarden, dus zelf zetten hoeft alleen buiten Compose |
|
||||||
| `JWT_EXPIRES` | `7d` | Geldigheidsduur token |
|
| `JWT_EXPIRES` | `7d` | Geldigheidsduur token |
|
||||||
| `CORS_ORIGIN` | `http://localhost` | Externe domeinnaam (één origin, dus zelden nodig) |
|
| `CORS_ORIGIN` | `http://localhost` | Externe domeinnaam (één origin, dus zelden nodig) |
|
||||||
| `APP_PORT` | `3400` | Poort op de host (container luistert op 3001) |
|
| `APP_PORT` | `3400` | Poort op de host (container luistert op 3001) |
|
||||||
@@ -276,6 +333,10 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
|
|||||||
|
|
||||||
- Wachtwoorden gehasht met bcrypt (cost factor 12)
|
- Wachtwoorden gehasht met bcrypt (cost factor 12)
|
||||||
- JWT authenticatie met instelbare vervaltijd
|
- JWT authenticatie met instelbare vervaltijd
|
||||||
|
- Data is per gebruiker: `/api/data` leest en schrijft alleen de rij van de
|
||||||
|
ingelogde gebruiker (`sub` uit het token)
|
||||||
|
- Van de rollen wordt alleen **Admin** afgedwongen, op `/api/users` — zie de
|
||||||
|
sectie Rollen in `BEHEER.md`
|
||||||
- Frontend en API op één origin — geen interne proxy, geen CORS-uitzonderingen
|
- Frontend en API op één origin — geen interne proxy, geen CORS-uitzonderingen
|
||||||
- Content-Security-Policy via Helmet, toegespitst op wat de app laadt
|
- Content-Security-Policy via Helmet, toegespitst op wat de app laadt
|
||||||
- Database bereikbaar alleen binnen het interne Docker netwerk
|
- Database bereikbaar alleen binnen het interne Docker netwerk
|
||||||
@@ -298,8 +359,9 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
|
|||||||
## 🛠️ Beheer
|
## 🛠️ Beheer
|
||||||
|
|
||||||
Zie `BEHEER.md` voor commando's voor lokale ontwikkeling, releases, logs en
|
Zie `BEHEER.md` voor commando's voor lokale ontwikkeling, releases, logs en
|
||||||
database-toegang — en voor de sectie **Tokens**: welk Gitea-token op welke
|
database-toegang — en voor de sectie **Tokens**: waarom één Gitea-token op het
|
||||||
machine hoort, met welke scopes, en wat een 401, 403 of 404 van Gitea betekent.
|
werkstation genoeg is, welke scopes het nodig heeft, en wat een 401, 403 of 404
|
||||||
|
van Gitea betekent.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vibefinance-frontend",
|
"name": "vibefinance-frontend",
|
||||||
"version": "1.0.1-dev",
|
"version": "1.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -209,14 +209,10 @@ function AppInner() {
|
|||||||
<div style={{ fontSize: 12, color: T.muted, lineHeight: 1.5 }}>
|
<div style={{ fontSize: 12, color: T.muted, lineHeight: 1.5 }}>
|
||||||
v{nieuweVersie.latest} is uitgebracht. Je gebruikt momenteel v{version}.
|
v{nieuweVersie.latest} is uitgebracht. Je gebruikt momenteel v{version}.
|
||||||
</div>
|
</div>
|
||||||
{nieuweVersie.notes && (
|
{/* De release-notes zelf staan hier niet: dat is Markdown
|
||||||
<div style={{
|
uit de changelog, en die kwam als ruwe `### `-koppen in
|
||||||
fontSize: 11, color: T.subtext, marginTop: 6, lineHeight: 1.5,
|
een smalle kolom te staan. Wie de details wil, volgt de
|
||||||
maxHeight: 130, overflowY: "auto", whiteSpace: "pre-wrap",
|
link naar de release. */}
|
||||||
}}>
|
|
||||||
{nieuweVersie.notes}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{nieuweVersie.url && (
|
{nieuweVersie.url && (
|
||||||
<a href={nieuweVersie.url} target="_blank" rel="noreferrer" style={{
|
<a href={nieuweVersie.url} target="_blank" rel="noreferrer" style={{
|
||||||
display: "inline-block", marginTop: 8,
|
display: "inline-block", marginTop: 8,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useApp } from "../context/AppContext.jsx";
|
import { useApp } from "../context/AppContext.jsx";
|
||||||
import { fmt, pct, fmtDatum } from "../utils/format.js";
|
import { fmt, pct, fmtDatum } from "../utils/format.js";
|
||||||
import { GREEN, RED } from "../constants/index.js";
|
import { GREEN, RED, GOLD } from "../constants/index.js";
|
||||||
|
|
||||||
const TEAL = "#a855f7";
|
const TEAL = "#a855f7";
|
||||||
import { VCard, VBadge } from "../components/ui/index.jsx";
|
import { VCard, VBadge } from "../components/ui/index.jsx";
|
||||||
@@ -38,6 +38,16 @@ export default function DashboardTab() {
|
|||||||
// Minigrafiek data
|
// Minigrafiek data
|
||||||
const vData = [...evRows].sort((a, b) => a.datum.localeCompare(b.datum));
|
const vData = [...evRows].sort((a, b) => a.datum.localeCompare(b.datum));
|
||||||
|
|
||||||
|
// Cumulatief geïnvesteerde fiat, op dezelfde datums als vData. Dezelfde
|
||||||
|
// opbouw als de KPI's hierboven: het oudste punt is de start-EV (daar is de
|
||||||
|
// inleg per definitie gelijk aan de waarde), elk volgend punt telt de
|
||||||
|
// stortingen van die rij erbij op. Het laatste punt is daarmee gelijk aan
|
||||||
|
// "Totaal geïnvesteerde fiat".
|
||||||
|
const fiatData = vData.reduce((acc, row, i) => {
|
||||||
|
acc.push(i === 0 ? (row.nettoWaarde || 0) : acc[i - 1] + catSum(row));
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
@@ -113,7 +123,21 @@ export default function DashboardTab() {
|
|||||||
|
|
||||||
{/* EV Grafiek */}
|
{/* EV Grafiek */}
|
||||||
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginBottom: 20 }}>
|
<VCard style={{ background: T.card, border: `1px solid ${T.border}`, marginBottom: 20 }}>
|
||||||
<div style={{ fontWeight: 600, marginBottom: 16 }}>Ontwikkeling van Eigen Vermogen</div>
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
gap: 12, flexWrap: "wrap", marginBottom: 16,
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: 600 }}>Ontwikkeling van Eigen Vermogen</div>
|
||||||
|
{vData.length >= 2 && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 11, color: T.muted }}>
|
||||||
|
<Legenda kleur="linear-gradient(90deg, #0eddd4, #8b5cf6)" label="Eigen vermogen" />
|
||||||
|
<Legenda
|
||||||
|
kleur={`repeating-linear-gradient(90deg, ${GOLD} 0 5px, transparent 5px 9px)`}
|
||||||
|
label="Geïnvesteerde fiat"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{vData.length < 2 ? (
|
{vData.length < 2 ? (
|
||||||
<div style={{ textAlign: "center", padding: "32px 0", color: T.muted }}>
|
<div style={{ textAlign: "center", padding: "32px 0", color: T.muted }}>
|
||||||
<div style={{ fontSize: 28, marginBottom: 8 }}>📈</div>
|
<div style={{ fontSize: 28, marginBottom: 8 }}>📈</div>
|
||||||
@@ -122,7 +146,7 @@ export default function DashboardTab() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<MiniLineChart vData={vData} darkMode={darkMode} />
|
<MiniLineChart vData={vData} fiatData={fiatData} darkMode={darkMode} />
|
||||||
)}
|
)}
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
||||||
@@ -130,6 +154,16 @@ export default function DashboardTab() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Legenda-item bij de grafiek ─────────────────────────────────────────────
|
||||||
|
function Legenda({ kleur, label }) {
|
||||||
|
return (
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||||
|
<span style={{ width: 16, height: 3, borderRadius: 2, background: kleur }} />
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Smooth Catmull-Rom bezier path ──────────────────────────────────────────
|
// ── Smooth Catmull-Rom bezier path ──────────────────────────────────────────
|
||||||
function crPath(xs, ys, t = 0.4) {
|
function crPath(xs, ys, t = 0.4) {
|
||||||
if (xs.length < 2) return "";
|
if (xs.length < 2) return "";
|
||||||
@@ -149,18 +183,25 @@ function crPath(xs, ys, t = 0.4) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
|
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
|
||||||
function MiniLineChart({ vData, darkMode }) {
|
function MiniLineChart({ vData, fiatData, darkMode }) {
|
||||||
const [hovered, setHovered] = useState(null);
|
const [hovered, setHovered] = useState(null);
|
||||||
const W = 700, H = 230, pl = 62, pr = 16, pt = 16, pb = 52;
|
const W = 700, H = 230, pl = 62, pr = 16, pt = 16, pb = 52;
|
||||||
const vals = vData.map((d) => d.nettoWaarde || 0);
|
const vals = vData.map((d) => d.nettoWaarde || 0);
|
||||||
const mn = Math.min(...vals) * 0.95;
|
const fVals = fiatData;
|
||||||
const mx = Math.max(...vals) * 1.05;
|
// Beide reeksen staan in euro's op dezelfde as, dus het bereik moet ze allebei
|
||||||
|
// omvatten — anders loopt de inleglijn buiten beeld zodra die boven de EV uitkomt.
|
||||||
|
const mn = Math.min(...vals, ...fVals) * 0.95;
|
||||||
|
const mx = Math.max(...vals, ...fVals) * 1.05;
|
||||||
const rng = mx - mn || 1;
|
const rng = mx - mn || 1;
|
||||||
const iW = W - pl - pr, iH = H - pt - pb;
|
const iW = W - pl - pr, iH = H - pt - pb;
|
||||||
const xs = vData.map((_, i) => pl + (i / Math.max(vData.length - 1, 1)) * iW);
|
const xs = vData.map((_, i) => pl + (i / Math.max(vData.length - 1, 1)) * iW);
|
||||||
const ys = vals.map((v) => pt + (1 - (v - mn) / rng) * iH);
|
const ys = vals.map((v) => pt + (1 - (v - mn) / rng) * iH);
|
||||||
|
const fYs = fVals.map((v) => pt + (1 - (v - mn) / rng) * iH);
|
||||||
const linePath = crPath(xs, ys);
|
const linePath = crPath(xs, ys);
|
||||||
const areaPath = `${linePath} L ${xs[xs.length-1]},${pt+iH} L ${xs[0]},${pt+iH} Z`;
|
const areaPath = `${linePath} L ${xs[xs.length-1]},${pt+iH} L ${xs[0]},${pt+iH} Z`;
|
||||||
|
// Rechte segmenten, geen crPath: de inleg loopt nooit terug, en de bezier-
|
||||||
|
// afronding zou tussen twee stortingen een dip tekenen die er niet is.
|
||||||
|
const fiatPath = xs.map((x, i) => `${i ? "L" : "M"} ${x},${fYs[i]}`).join(" ");
|
||||||
const gc = darkMode ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.06)";
|
const gc = darkMode ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.06)";
|
||||||
const lc = darkMode ? "#6b7280" : "#94a3b8";
|
const lc = darkMode ? "#6b7280" : "#94a3b8";
|
||||||
|
|
||||||
@@ -172,9 +213,19 @@ function MiniLineChart({ vData, darkMode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const h = hovered;
|
const h = hovered;
|
||||||
const tipW = 114, tipH = 46;
|
const tipW = 150, tipH = 76;
|
||||||
const tipX = h !== null ? Math.min(Math.max(xs[h] - tipW / 2, pl), W - tipW - 4) : 0;
|
// Náást de hoverlijn, niet erboven of eronder. Zodra de twee punten ver uit
|
||||||
const tipY = h !== null ? (ys[h] - tipH - 14 < pt ? ys[h] + 14 : ys[h] - tipH - 14) : 0;
|
// elkaar liggen is er verticaal geen ruimte voor een kaart van 76px: dan
|
||||||
|
// klapte hij naar onderen tot buiten de tekening, over de datumlabels heen en
|
||||||
|
// half afgekapt. Zijwaarts past hij altijd; de klemmen houden hem binnen het
|
||||||
|
// tekenvlak.
|
||||||
|
const naarRechts = h !== null && xs[h] < pl + iW / 2;
|
||||||
|
const tipX = h === null ? 0 : naarRechts
|
||||||
|
? Math.min(xs[h] + 16, W - pr - tipW)
|
||||||
|
: Math.max(xs[h] - tipW - 16, pl);
|
||||||
|
const tipY = h === null ? 0
|
||||||
|
: Math.max(pt, Math.min((ys[h] + fYs[h]) / 2 - tipH / 2, pt + iH - tipH));
|
||||||
|
const resultaat = h !== null ? vals[h] - fVals[h] : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -214,6 +265,11 @@ function MiniLineChart({ vData, darkMode }) {
|
|||||||
{/* Vlakkleur met gradient + fade */}
|
{/* Vlakkleur met gradient + fade */}
|
||||||
<path d={areaPath} fill="url(#chartLineGrad)" mask="url(#chartAreaMask)" opacity="0.4" />
|
<path d={areaPath} fill="url(#chartLineGrad)" mask="url(#chartAreaMask)" opacity="0.4" />
|
||||||
|
|
||||||
|
{/* Geïnvesteerde fiat — gestippeld, zodat het als referentielijn leest
|
||||||
|
en de EV-lijn de hoofdrol houdt */}
|
||||||
|
<path d={fiatPath} fill="none" stroke={GOLD} strokeWidth="1.8"
|
||||||
|
strokeDasharray="5,4" strokeLinecap="round" strokeLinejoin="round" opacity="0.85" />
|
||||||
|
|
||||||
{/* Lijn */}
|
{/* Lijn */}
|
||||||
<path d={linePath} fill="none" stroke="url(#chartLineGrad)"
|
<path d={linePath} fill="none" stroke="url(#chartLineGrad)"
|
||||||
strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
@@ -224,10 +280,14 @@ function MiniLineChart({ vData, darkMode }) {
|
|||||||
stroke="rgba(255,255,255,0.25)" strokeWidth="1" strokeDasharray="4,3" />
|
stroke="rgba(255,255,255,0.25)" strokeWidth="1" strokeDasharray="4,3" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hover punt */}
|
{/* Hover punten */}
|
||||||
{h !== null && (
|
{h !== null && (
|
||||||
|
<>
|
||||||
|
<circle cx={xs[h]} cy={fYs[h]} r="4.5"
|
||||||
|
fill={GOLD} stroke={darkMode ? "#151c2e" : "#fff"} strokeWidth="2.5" />
|
||||||
<circle cx={xs[h]} cy={ys[h]} r="5.5"
|
<circle cx={xs[h]} cy={ys[h]} r="5.5"
|
||||||
fill="#0eddd4" stroke={darkMode ? "#151c2e" : "#fff"} strokeWidth="2.5" />
|
fill="#0eddd4" stroke={darkMode ? "#151c2e" : "#fff"} strokeWidth="2.5" />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tooltip */}
|
{/* Tooltip */}
|
||||||
@@ -239,10 +299,24 @@ function MiniLineChart({ vData, darkMode }) {
|
|||||||
<text x={tipX + tipW / 2} y={tipY + 16} fill={lc} fontSize="9" textAnchor="middle">
|
<text x={tipX + tipW / 2} y={tipY + 16} fill={lc} fontSize="9" textAnchor="middle">
|
||||||
{fmtDatum(vData[h].datum)}
|
{fmtDatum(vData[h].datum)}
|
||||||
</text>
|
</text>
|
||||||
<text x={tipX + tipW / 2} y={tipY + 34} fill="#0eddd4" fontSize="11"
|
<text x={tipX + 10} y={tipY + 33} fill={lc} fontSize="9">Vermogen</text>
|
||||||
fontWeight="700" textAnchor="middle">
|
<text x={tipX + tipW - 10} y={tipY + 33} fill="#0eddd4" fontSize="10"
|
||||||
|
fontWeight="700" textAnchor="end">
|
||||||
{fmt(vals[h])}
|
{fmt(vals[h])}
|
||||||
</text>
|
</text>
|
||||||
|
<text x={tipX + 10} y={tipY + 48} fill={lc} fontSize="9">Ingelegd</text>
|
||||||
|
<text x={tipX + tipW - 10} y={tipY + 48} fill={GOLD} fontSize="10"
|
||||||
|
fontWeight="700" textAnchor="end">
|
||||||
|
{fmt(fVals[h])}
|
||||||
|
</text>
|
||||||
|
<line x1={tipX + 10} y1={tipY + 55} x2={tipX + tipW - 10} y2={tipY + 55}
|
||||||
|
stroke={gc} strokeWidth="1" />
|
||||||
|
<text x={tipX + 10} y={tipY + 68} fill={lc} fontSize="9">Resultaat</text>
|
||||||
|
<text x={tipX + tipW - 10} y={tipY + 68}
|
||||||
|
fill={resultaat >= 0 ? GREEN : RED} fontSize="10"
|
||||||
|
fontWeight="700" textAnchor="end">
|
||||||
|
{`${resultaat >= 0 ? "+" : ""}${fmt(resultaat)}`}
|
||||||
|
</text>
|
||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vibefinance",
|
"name": "vibefinance",
|
||||||
"version": "1.0.1",
|
"version": "1.0.7",
|
||||||
"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",
|
||||||
|
|||||||
+170
-44
@@ -9,6 +9,16 @@
|
|||||||
# .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
|
# .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||||
# .\release.ps1 -DryRun # toon alle commando's zonder iets uit te voeren
|
# .\release.ps1 -DryRun # toon alle commando's zonder iets uit te voeren
|
||||||
# .\release.ps1 -Force # sla conflictcontrole over, overschrijf tags
|
# .\release.ps1 -Force # sla conflictcontrole over, overschrijf tags
|
||||||
|
# .\release.ps1 -NoChangelog # publiceer bewust zonder release-notes
|
||||||
|
#
|
||||||
|
# Het script werkt zelf vier dingen bij, dus die hoef je niet met de hand te
|
||||||
|
# doen: de changelog-kop `## [Niet vrijgegeven]` -> `## [x.x.x] - datum`, de
|
||||||
|
# versie in frontend/package.json, de versiebadge in README.md, en ná het
|
||||||
|
# publiceren de versie in de root package.json -> `x.x.(x+1)-dev`.
|
||||||
|
#
|
||||||
|
# LET OP bij -Force: dat verwijdert de bestaande tag lokaal en op origin, en
|
||||||
|
# Gitea gooit de bijbehorende release daarmee weg. Onder -DryRun wordt dat
|
||||||
|
# alleen gemeld (dat was eerder niet zo — zie stap 4).
|
||||||
#
|
#
|
||||||
# Vereisten:
|
# Vereisten:
|
||||||
# - Docker Desktop actief
|
# - Docker Desktop actief
|
||||||
@@ -16,6 +26,9 @@
|
|||||||
# - docker buildx builder actief: docker buildx create --use
|
# - docker buildx builder actief: docker buildx create --use
|
||||||
# - Git geinstalleerd en working directory is de repo root
|
# - Git geinstalleerd en working directory is de repo root
|
||||||
# - root package.json bevat een schoon versienummer ZONDER -dev suffix
|
# - root package.json bevat een schoon versienummer ZONDER -dev suffix
|
||||||
|
# - Changelog.md beschrijft deze versie, onder `## [Niet vrijgegeven]` of
|
||||||
|
# onder een kop `## [x.x.x]`; anders breekt het script af (-NoChangelog
|
||||||
|
# omzeilt dat)
|
||||||
# - GITEA_TOKEN in de omgeving (token met release-rechten) voor stap 5,
|
# - GITEA_TOKEN in de omgeving (token met release-rechten) voor stap 5,
|
||||||
# de Gitea-release waar de update-melding in de app op afgaat
|
# de Gitea-release waar de update-melding in de app op afgaat
|
||||||
|
|
||||||
@@ -23,7 +36,10 @@ param(
|
|||||||
[switch]$NoBuild,
|
[switch]$NoBuild,
|
||||||
[switch]$SkipTests,
|
[switch]$SkipTests,
|
||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[switch]$Force
|
[switch]$Force,
|
||||||
|
# Releasen zonder inhoudelijke release-notes. Zonder deze switch breekt het
|
||||||
|
# script af wanneer de changelog niets over deze versie zegt.
|
||||||
|
[switch]$NoChangelog
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -75,42 +91,13 @@ if ($appVersion -match "-dev$") {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Versie synchroniseren naar frontend/package.json ──────────────────────
|
# Git-tag met `v`-prefix, net als de release-naam hieronder en net als de tags
|
||||||
# Een droogloop schrijft hier niet: anders laat -DryRun een gewijzigd bestand
|
# t/m v1.0.0. Gitea toont de tag op de release-pagina, dus zonder prefix stond
|
||||||
# achter, en tagt de echte release daarna een vervuilde working tree.
|
# daar het kale nummer. De image-tags in de registry houden bewust géén `v`:
|
||||||
$frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json"
|
# dat is de conventie voor container-tags.
|
||||||
$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json
|
# De update-check verdraagt beide: zowel backend/src/routes/update.js als
|
||||||
if ($frontendPkg.version -ne $appVersion) {
|
# useUpdateCheck.js strippen een leidende `v` voordat ze versies vergelijken.
|
||||||
if ($DryRun) {
|
$gitTag = "v$appVersion"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$gitTag = "$appVersion"
|
|
||||||
|
|
||||||
# ── Registry tags (één image voor frontend + backend) ─────────────────────
|
# ── Registry tags (één image voor frontend + backend) ─────────────────────
|
||||||
$appBase = "$registry/$appImage"
|
$appBase = "$registry/$appImage"
|
||||||
@@ -151,12 +138,13 @@ function Write-Step {
|
|||||||
Write-Host "── $Label" -ForegroundColor Cyan
|
Write-Host "── $Label" -ForegroundColor Cyan
|
||||||
}
|
}
|
||||||
|
|
||||||
# Haalt de sectie van deze versie uit Changelog.md als release-notes.
|
# Haalt de sectie van deze versie uit changelog-tekst. Werkt op een string en
|
||||||
function Get-ChangelogSection {
|
# niet op het bestand, zodat een droogloop kan valideren tegen de kop die hij
|
||||||
param([string]$Version)
|
# zou schrijven zonder die daadwerkelijk te schrijven.
|
||||||
$path = Join-Path $PSScriptRoot "Changelog.md"
|
function Get-ChangelogSectionFromText {
|
||||||
if (-not (Test-Path $path)) { return "" }
|
param([string]$Text, [string]$Version)
|
||||||
$lines = Get-Content $path
|
if (-not $Text) { return "" }
|
||||||
|
$lines = $Text -split "\r?\n"
|
||||||
$start = -1
|
$start = -1
|
||||||
$needle = "^##\s*\[" + [regex]::Escape($Version) + "\]"
|
$needle = "^##\s*\[" + [regex]::Escape($Version) + "\]"
|
||||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||||
@@ -172,6 +160,99 @@ function Get-ChangelogSection {
|
|||||||
return (($out -join "`n").Trim())
|
return (($out -join "`n").Trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Changelog-kop vrijgeven ───────────────────────────────────────────────
|
||||||
|
# De release-notes komen uit de sectie `## [x.x.x]` in Changelog.md. Stond het
|
||||||
|
# werk nog onder `## [Niet vrijgegeven]`, dan vond het script niets en werd stil
|
||||||
|
# de placeholder "Release x.x.x" gepubliceerd — dat gebeurde bij 0.1.9 en 1.0.2,
|
||||||
|
# en is achteraf alleen met een PATCH op de release te herstellen. Daarom zet het
|
||||||
|
# script de kop nu zelf om, en breekt het af als er niets te melden valt.
|
||||||
|
# Zelfde regel als bij de andere auto-updates: een droogloop meldt, maar schrijft
|
||||||
|
# niet.
|
||||||
|
$changelogPath = Join-Path $PSScriptRoot "Changelog.md"
|
||||||
|
$nieuweKop = "## [$appVersion] - $(Get-Date -Format 'yyyy-MM-dd')"
|
||||||
|
$changelogTxt = if (Test-Path $changelogPath) { [System.IO.File]::ReadAllText($changelogPath) } else { "" }
|
||||||
|
|
||||||
|
$kopPatroon = "(?m)^##\s*\[" + [regex]::Escape($appVersion) + "\]"
|
||||||
|
# Niet `\s*$`: `\s` matcht ook newlines, en dan slokt de match de lege regel ná
|
||||||
|
# de kop op — die verdween daardoor bij het omzetten. En niet `[ \t]*$` alleen:
|
||||||
|
# dit bestand heeft CRLF (zie .gitattributes), dus `$` staat achter een `\r` die
|
||||||
|
# geen space of tab is. De lookahead laat het regeleinde buiten de match, zodat
|
||||||
|
# de CRLF intact blijft.
|
||||||
|
$onderwegPatroon = "(?m)^##[ \t]*\[[ \t]*Niet vrijgegeven[ \t]*\][ \t]*(?=\r?$)"
|
||||||
|
|
||||||
|
# Eerst alleen in het geheugen omzetten. Schrijven gebeurt na de controle
|
||||||
|
# hieronder: bij een lege `[Niet vrijgegeven]`-sectie zou de kop anders al
|
||||||
|
# vervangen zijn terwijl het script daarna afbreekt, en dan blijft er een
|
||||||
|
# aangepaste changelog achter bij een release die niet doorging.
|
||||||
|
$kopOmzetten = ($changelogTxt -notmatch $kopPatroon -and $changelogTxt -match $onderwegPatroon)
|
||||||
|
if ($kopOmzetten) {
|
||||||
|
# $nieuweKop bevat geen '$', dus veilig als vervangingstekst.
|
||||||
|
$changelogTxt = [regex]::Replace($changelogTxt, $onderwegPatroon, $nieuweKop)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-ChangelogSectionFromText $changelogTxt $appVersion) -and -not $NoChangelog) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Changelog.md zegt niets over $appVersion." -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Zonder notes komt er alleen de tekst 'Release $appVersion' in de Gitea-release," -ForegroundColor Yellow
|
||||||
|
Write-Host "en dat is achteraf alleen met een PATCH op de release te herstellen." -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Zet je wijzigingen in Changelog.md onder een kop:" -ForegroundColor White
|
||||||
|
Write-Host " ## [Niet vrijgegeven]" -ForegroundColor White
|
||||||
|
Write-Host "Die zet dit script bij de volgende run zelf om naar '$nieuweKop'." -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Bewust zonder release-notes publiceren? Draai met -NoChangelog." -ForegroundColor DarkGray
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Er is niets gepubliceerd." -ForegroundColor DarkGray
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# De controle is doorstaan, dus nu pas schrijven.
|
||||||
|
if ($kopOmzetten) {
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host "[DRY-RUN] Changelog-kop vrijgeven: '## [Niet vrijgegeven]' -> '$nieuweKop'" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
[System.IO.File]::WriteAllText($changelogPath, $changelogTxt)
|
||||||
|
Write-Host "Changelog-kop vrijgegeven: $nieuweKop" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Versie synchroniseren naar frontend/package.json ──────────────────────
|
||||||
|
# Staat na de changelog-controle: anders laat een afgebroken release wél een
|
||||||
|
# gewijzigde working tree achter terwijl hij "niets gepubliceerd" meldt.
|
||||||
|
# Een droogloop schrijft hier niet, om dezelfde reden.
|
||||||
|
$frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json"
|
||||||
|
$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json
|
||||||
|
if ($frontendPkg.version -ne $appVersion) {
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host "[DRY-RUN] Versie naar frontend synchroniseren: $($frontendPkg.version) -> $appVersion" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
$frontendPkg.version = $appVersion
|
||||||
|
[System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10))
|
||||||
|
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Versiebadge in README.md ──────────────────────────────────────────────
|
||||||
|
# Anders loopt die badge stil achter: hij stond drie releases achter voordat
|
||||||
|
# dit erin zat, en het script kent het nummer toch al. Zelfde regel als
|
||||||
|
# hierboven: een droogloop meldt het, maar schrijft niet.
|
||||||
|
$readmePath = Join-Path $PSScriptRoot "README.md"
|
||||||
|
if (Test-Path $readmePath) {
|
||||||
|
$readme = [System.IO.File]::ReadAllText($readmePath)
|
||||||
|
$patroon = "badge/versie-[^-]+-"
|
||||||
|
$nieuw = "badge/versie-$appVersion-"
|
||||||
|
$huidig = [regex]::Match($readme, $patroon)
|
||||||
|
if ($huidig.Success -and $huidig.Value -ne $nieuw) {
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host "[DRY-RUN] Versiebadge in README.md: $($huidig.Value) -> $nieuw" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
[System.IO.File]::WriteAllText($readmePath, ($readme -replace $patroon, $nieuw))
|
||||||
|
Write-Host "Versiebadge in README.md bijgewerkt: $appVersion" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# ── Samenvatting ───────────────────────────────────────────────────────────
|
# ── Samenvatting ───────────────────────────────────────────────────────────
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
@@ -365,14 +446,24 @@ if (-not $NoBuild) {
|
|||||||
Write-Step "[4/5] Git-tag aanmaken en pushen"
|
Write-Step "[4/5] Git-tag aanmaken en pushen"
|
||||||
|
|
||||||
if ($Force) {
|
if ($Force) {
|
||||||
|
# LET OP: deze twee commando's gingen niet via Invoke-Cmd en hadden dus geen
|
||||||
|
# -DryRun-controle. Een `-DryRun -Force` verwijderde daardoor écht de tag,
|
||||||
|
# lokaal en op origin — en Gitea ruimt de release op zodra de tag verdwijnt.
|
||||||
|
# Zo is de release van 1.0.2 verloren gegaan.
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host "[DRY-RUN] git tag -d $gitTag" -ForegroundColor Yellow
|
||||||
|
Write-Host "[DRY-RUN] git push origin :refs/tags/$gitTag" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
|
Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
|
||||||
$ErrorActionPreference = "SilentlyContinue"
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
git tag -d $gitTag 2>$null | Out-Null
|
git tag -d $gitTag 2>$null | Out-Null
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
Write-Host "Bestaande remote tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
|
Write-Host "Bestaande remote tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
|
||||||
|
Write-Host " Gitea verwijdert de bijbehorende release mee." -ForegroundColor DarkGray
|
||||||
$ErrorActionPreference = "SilentlyContinue"
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
git push origin ":refs/tags/$gitTag" 2>$null | Out-Null
|
git push origin ":refs/tags/$gitTag" 2>$null | Out-Null
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Git-tag aanmaken: $gitTag" -ForegroundColor White
|
Write-Host "Git-tag aanmaken: $gitTag" -ForegroundColor White
|
||||||
@@ -405,7 +496,10 @@ if (-not $releaseApi) {
|
|||||||
Write-Host "Geen GITEA_TOKEN in de omgeving — release overgeslagen." -ForegroundColor Yellow
|
Write-Host "Geen GITEA_TOKEN in de omgeving — release overgeslagen." -ForegroundColor Yellow
|
||||||
Write-Host " Zonder release ziet de app geen update-melding voor $appVersion." -ForegroundColor DarkGray
|
Write-Host " Zonder release ziet de app geen update-melding voor $appVersion." -ForegroundColor DarkGray
|
||||||
} else {
|
} else {
|
||||||
$notes = Get-ChangelogSection $appVersion
|
# Uit $changelogTxt en niet opnieuw van schijf: in een droogloop is de kop
|
||||||
|
# nog niet geschreven, en dan zou de payload hier de placeholder tonen —
|
||||||
|
# precies wat je met een droogloop wilt uitsluiten.
|
||||||
|
$notes = Get-ChangelogSectionFromText $changelogTxt $appVersion
|
||||||
if (-not $notes) { $notes = "Release $appVersion" }
|
if (-not $notes) { $notes = "Release $appVersion" }
|
||||||
$payload = @{
|
$payload = @{
|
||||||
tag_name = $gitTag
|
tag_name = $gitTag
|
||||||
@@ -451,6 +545,38 @@ Write-Host " Git-tag: $gitTag -> origin" -ForegroundColor Green
|
|||||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
|
# ── Versie naar de volgende dev-cyclus ────────────────────────────────────
|
||||||
|
# Was voorheen stap 4 met de hand. Gebeurt hier, ná het publiceren, zodat een
|
||||||
|
# afgebroken release het versienummer niet vooruit zet. Bijkomend voordeel: de
|
||||||
|
# `-dev` suffix laat dezelfde release niet nog eens draaien (de controle boven
|
||||||
|
# in dit script weigert een `-dev` versie), dus een dubbele publicatie is
|
||||||
|
# meteen uitgesloten. Wil je bewust opnieuw releasen, zet het nummer dan zelf
|
||||||
|
# terug.
|
||||||
|
if ($appVersion -match '^(\d+)\.(\d+)\.(\d+)$') {
|
||||||
|
$volgende = "{0}.{1}.{2}-dev" -f $Matches[1], $Matches[2], ([int]$Matches[3] + 1)
|
||||||
|
$rootPkgPath = Join-Path $PSScriptRoot "package.json"
|
||||||
|
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host "[DRY-RUN] Versie in package.json: $appVersion -> $volgende" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
# Gerichte tekstvervanging in plaats van ConvertTo-Json: dat zou het hele
|
||||||
|
# bestand herschrijven en de opmaak van de andere velden aantasten.
|
||||||
|
$rootTxt = [System.IO.File]::ReadAllText($rootPkgPath)
|
||||||
|
$treffer = [regex]::Match($rootTxt, '"version"\s*:\s*"' + [regex]::Escape($appVersion) + '"')
|
||||||
|
if ($treffer.Success) {
|
||||||
|
$rootTxt = $rootTxt.Remove($treffer.Index, $treffer.Length).Insert($treffer.Index, '"version": "' + $volgende + '"')
|
||||||
|
[System.IO.File]::WriteAllText($rootPkgPath, $rootTxt)
|
||||||
|
Write-Host "Versie in package.json naar de volgende cyclus: $volgende" -ForegroundColor Green
|
||||||
|
Write-Host " Commit dit mee, samen met de changelog-kop en de README-badge." -ForegroundColor DarkGray
|
||||||
|
Write-Host ""
|
||||||
|
} else {
|
||||||
|
Write-Host "Kon '$appVersion' niet terugvinden in package.json — versie niet bijgewerkt." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "Versie '$appVersion' is geen x.y.z — volgende dev-versie niet automatisch gezet." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
# Expliciete 0: anders erft de caller de exitcode van het laatste
|
# Expliciete 0: anders erft de caller de exitcode van het laatste
|
||||||
# docker-commando (bijv. de registry-check).
|
# docker-commando (bijv. de registry-check).
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
# ./release.sh -SkipTests # sla de testsuite over (niet aanbevolen)
|
# ./release.sh -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||||
# ./release.sh -DryRun # toon alle commando's zonder iets uit te voeren
|
# ./release.sh -DryRun # toon alle commando's zonder iets uit te voeren
|
||||||
# ./release.sh -Force # sla conflictcontrole over, overschrijf tags
|
# ./release.sh -Force # sla conflictcontrole over, overschrijf tags
|
||||||
|
# ./release.sh -NoChangelog # publiceer bewust zonder release-notes
|
||||||
#
|
#
|
||||||
# Combinaties:
|
# Combinaties:
|
||||||
# ./release.sh -Force -NoBuild
|
# ./release.sh -Force -NoBuild
|
||||||
|
|||||||
Reference in New Issue
Block a user