Compare commits
3
Commits
v1.0.7
..
796245fefe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
796245fefe | ||
|
|
0bbb166b32 | ||
|
|
fe6b94aa7f |
@@ -245,11 +245,9 @@ tag op een Raspberry Pi en op een x86-server werkt.
|
|||||||
| `README.md` | de `versie-x.x.x`-badge bovenaan | 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 |
|
| `package.json` (root) | versie → `x.x.(x+1)-dev` voor de volgende cyclus | **ná** het publiceren |
|
||||||
|
|
||||||
Een `-DryRun` meldt de vier bestandswijzigingen zonder ze te schrijven (de
|
Een `-DryRun` meldt alle vier zonder te schrijven. Ze blijven na een echte
|
||||||
Git-tag is de uitzondering in die tabel: die hoort bij stap 4, niet bij de
|
release als wijziging in je working tree staan; neem ze mee in de volgende
|
||||||
auto-updates). Na een echte release blijven die vier als wijziging in je working
|
commit.
|
||||||
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 laatste staat bewust ná het publiceren: breekt de release eerder af, dan
|
De laatste staat bewust ná het publiceren: breekt de release eerder af, dan
|
||||||
schuift het versienummer niet vooruit. Bijeffect dat je moet kennen — na een
|
schuift het versienummer niet vooruit. Bijeffect dat je moet kennen — na een
|
||||||
@@ -335,11 +333,9 @@ 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, `Cache-Control: no-store`) |
|
| `/api/...` | REST API (rate limited) |
|
||||||
| `/assets/...` | gehashte build-bestanden uit `STATIC_DIR`, 1 jaar `immutable` |
|
| `/assets/...` | statische build uit `STATIC_DIR`, 1 jaar `immutable` gecached |
|
||||||
| `/favicon.svg`, `/icons/...` | vaste namen, dus `max-age=0, must-revalidate` |
|
| al het overige | `index.html` — fallback voor React-routing |
|
||||||
| 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
|
||||||
@@ -385,12 +381,6 @@ 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
|
||||||
@@ -417,25 +407,12 @@ 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` | Enige rol die wordt gecontroleerd: `requireAdmin` in `middleware/auth.js` bewaakt alle routes onder `/api/users` |
|
| `Admin` | Alles, inclusief gebruikersbeheer |
|
||||||
| `Bewerker` | Geen eigen rechten — gelijk aan `Viewer` |
|
| `Bewerker` | Data lezen en bewerken |
|
||||||
| `Viewer` | Standaardrol voor een nieuw account; mag de eigen data lezen én schrijven |
|
| `Viewer` | Alleen lezen |
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,79 +2,6 @@
|
|||||||
|
|
||||||
## [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
|
### Gewijzigd
|
||||||
- Git-tags krijgen weer een `v`-prefix (`v1.0.4`), net als t/m `v1.0.0`. Sinds
|
- 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
|
1.0.1 was dat `1.0.1`, `1.0.2`, `1.0.3` — en omdat Gitea de tag op de
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -166,30 +166,16 @@ build — anders zou de release met een placeholder als notes online komen.
|
|||||||
|
|
||||||
| Scherm | Wat het doet |
|
| Scherm | Wat het doet |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 📊 **Dashboard** | KPI's over inleg, vermogen, winst en ROI, de voortgang naar je doel, en de grafiek met vermogen tegen inleg |
|
| 📊 **Dashboard** | Overzicht van totaal eigen vermogen, schulden en netto vermogen |
|
||||||
| 💰 **Eigen vermogen** | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek |
|
| 💰 **Eigen vermogen** | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek |
|
||||||
| 📉 **Schuld** | Schulden bijhouden per blok met afbetalingsoverzicht |
|
| 📉 **Schulden** | Schulden bijhouden per blok met afbetalingsoverzicht |
|
||||||
| 📈 **Voortgang Schuld & EV** | 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) |
|
||||||
| ⚙️ **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
|
||||||
@@ -207,33 +193,27 @@ 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)
|
│ ├── 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/ # format.js (bedragen/datums), storage.js
|
│ ├── utils/ # Formatters, helpers
|
||||||
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
|
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
|
||||||
│ ├── context/ # AppContext (global state + API sync + backup)
|
│ ├── context/ # AppContext (global state + API sync)
|
||||||
│ ├── 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
|
||||||
@@ -246,11 +226,10 @@ 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
|
├── routes/ # auth, data, users, health, update-check
|
||||||
└── stores/ # userStore, dataStore
|
└── stores/ # userStore, dataStore
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -266,11 +245,9 @@ frontend en API.
|
|||||||
|
|
||||||
| Verzoek | Wat de app doet |
|
| Verzoek | Wat de app doet |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/api/...` | REST API (rate limited, `Cache-Control: no-store`) |
|
| `/api/...` | REST API (rate limited) |
|
||||||
| `/assets/...` | Gehashte build-bestanden, 1 jaar `immutable` gecached |
|
| `/assets/...` | Statische build, 1 jaar `immutable` gecached |
|
||||||
| `/favicon.svg`, `/icons/...` | Vaste namen, dus `max-age=0, must-revalidate` |
|
| al het overige | `index.html` — fallback voor React-routing |
|
||||||
| 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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -318,7 +295,6 @@ 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) |
|
||||||
@@ -333,10 +309,6 @@ 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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vibefinance-frontend",
|
"name": "vibefinance-frontend",
|
||||||
"version": "1.0.6",
|
"version": "1.0.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
@@ -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, GOLD } from "../constants/index.js";
|
import { GREEN, RED } 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,16 +38,6 @@ 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 }}>
|
||||||
@@ -123,21 +113,7 @@ 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={{
|
<div style={{ fontWeight: 600, marginBottom: 16 }}>Ontwikkeling van Eigen Vermogen</div>
|
||||||
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>
|
||||||
@@ -146,7 +122,7 @@ export default function DashboardTab() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<MiniLineChart vData={vData} fiatData={fiatData} darkMode={darkMode} />
|
<MiniLineChart vData={vData} darkMode={darkMode} />
|
||||||
)}
|
)}
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
||||||
@@ -154,16 +130,6 @@ 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 "";
|
||||||
@@ -183,25 +149,18 @@ function crPath(xs, ys, t = 0.4) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
|
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
|
||||||
function MiniLineChart({ vData, fiatData, darkMode }) {
|
function MiniLineChart({ vData, 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 fVals = fiatData;
|
const mn = Math.min(...vals) * 0.95;
|
||||||
// Beide reeksen staan in euro's op dezelfde as, dus het bereik moet ze allebei
|
const mx = Math.max(...vals) * 1.05;
|
||||||
// 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";
|
||||||
|
|
||||||
@@ -213,19 +172,9 @@ function MiniLineChart({ vData, fiatData, darkMode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const h = hovered;
|
const h = hovered;
|
||||||
const tipW = 150, tipH = 76;
|
const tipW = 114, tipH = 46;
|
||||||
// Náást de hoverlijn, niet erboven of eronder. Zodra de twee punten ver uit
|
const tipX = h !== null ? Math.min(Math.max(xs[h] - tipW / 2, pl), W - tipW - 4) : 0;
|
||||||
// elkaar liggen is er verticaal geen ruimte voor een kaart van 76px: dan
|
const tipY = h !== null ? (ys[h] - tipH - 14 < pt ? ys[h] + 14 : ys[h] - tipH - 14) : 0;
|
||||||
// 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
|
||||||
@@ -265,11 +214,6 @@ function MiniLineChart({ vData, fiatData, 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" />
|
||||||
@@ -280,14 +224,10 @@ function MiniLineChart({ vData, fiatData, 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 punten */}
|
{/* Hover punt */}
|
||||||
{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 */}
|
||||||
@@ -299,24 +239,10 @@ function MiniLineChart({ vData, fiatData, 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 + 10} y={tipY + 33} fill={lc} fontSize="9">Vermogen</text>
|
<text x={tipX + tipW / 2} y={tipY + 34} fill="#0eddd4" fontSize="11"
|
||||||
<text x={tipX + tipW - 10} y={tipY + 33} fill="#0eddd4" fontSize="10"
|
fontWeight="700" textAnchor="middle">
|
||||||
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.7",
|
"version": "1.0.4",
|
||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user