2026-09-04 09:19:14 +02:00
<div align="center">
# 💜 VibeFinance
**Persoonlijk vermogensdashboard**
Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer
2026-09-09 17:36:34 +02:00

2026-09-04 09:19:14 +02:00



2026-09-11 15:20:09 +02:00

2026-09-04 09:19:14 +02:00

2026-09-09 17:36:34 +02:00
<img src="docs/screenshot-dashboard.png" alt="VibeFinance dashboard" width="820">
2026-09-04 09:19:14 +02:00
</div>
---
## 🤖 Gebouwd met Claude.ai
Deze applicatie is volledig ontworpen en ontwikkeld met behulp van [Claude.ai ](https://claude.ai ) — de AI-assistent van Anthropic. Van architectuur tot gebruikersinterface: alles is stap voor stap tot stand gekomen in samenwerking met Claude.
---
## ⚡ Quick Start
> Vereisten: Docker Engine 24+ en Docker Compose v2+
2026-09-04 11:51:29 +02:00
De app draait in **2 containers** : `app` (Express API + gebouwde frontend) en
`postgres` (database).
2026-09-10 16:24:30 +02:00
**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:
2026-09-04 09:19:14 +02:00
```env
POSTGRES_USER = vibefinance
POSTGRES_PASSWORD = kies_een_sterk_wachtwoord
JWT_SECRET = minimaal_64_willekeurige_tekens
2026-09-04 11:51:29 +02:00
APP_PORT = 3400
2026-09-04 09:19:14 +02:00
```
```bash
# Genereer een veilige JWT_SECRET
openssl rand -hex 64
```
2026-09-10 16:24:30 +02:00
**3. Starten:**
2026-09-04 09:19:14 +02:00
```bash
2026-09-04 11:51:29 +02:00
cp .env.example .env # en vul de waarden in
2026-09-09 17:36:34 +02:00
2026-09-04 09:19:14 +02:00
docker compose pull && docker compose up -d
```
2026-09-10 16:24:30 +02:00
Twee foutmeldingen en wat ze betekenen:
2026-09-10 14:03:34 +02:00
2026-09-10 16:24:30 +02:00
- **`no configuration file provided: not found` ** — je staat in een map zonder
`docker-compose.yml` . Controleer met `ls docker-compose.yml` , of geef het
bestand expliciet mee: `docker compose -f /pad/naar/docker-compose.yml up -d` .
- **`401 Unauthorized` ** of ** "pull access denied"** — de organisatie staat niet
meer op publiek. Dan alsnog inloggen:
```bash
docker login vibehoogie.duckdns.org # wachtwoord = Gitea-token met read:package
` ``
2026-09-10 14:03:34 +02:00
2026-09-04 11:51:29 +02:00
Navigeer naar ` http://<server-ip>:3400` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol.
De container luistert intern op poort 3001 en serveert daar zowel de frontend
(op ` /`) als de API (op ` /api`). ` APP_PORT` bepaalt de poort op de host.
2026-09-04 09:19:14 +02:00
### Development (hot reload)
` ``powershell
.\dev.ps1
` ``
2026-09-04 11:51:29 +02:00
Eén dev-container draait de API (` node --watch`) én de Vite dev-server samen:
| Adres | Wat |
|---|---|
| http://localhost | de app in de browser (Vite, hot reload) |
| http://localhost:3001/api/health | de API rechtstreeks |
Vite proxyt ` /api` naar de API in dezelfde container, dus je hebt geen CORS-
uitzonderingen nodig. Handige acties:
` ``powershell
.\dev.ps1 up -Target app # alleen de app herbouwen en herstarten
.\dev.ps1 logs # logs van de app volgen
.\dev.ps1 down # stoppen én volumes wissen (dev-database weg)
.\dev.ps1 test # testsuite draaien
` ``
> ` .\dev.ps1 up` laat de dev-database staan; alleen ` .\dev.ps1 down` wist de volumes.
### Testen
` ``powershell
2026-09-04 12:21:06 +02:00
.\test.ps1 # dependency-check + backend-tests + build + smoketest
.\test.ps1 -Unit # dependency-check + backend-tests
.\test.ps1 -NoSmoke # zonder de stack te starten
.\test.ps1 -Strict # faal als een dependency achterloopt
.\test.ps1 -SkipDeps # dependency-check overslaan (bijv. offline)
2026-09-04 11:51:29 +02:00
` ``
Alles loopt in Docker — Node op de host is niet nodig. De backend-tests draaien
zonder database via het ` test`-target in de ` Dockerfile`.
2026-09-04 12:21:06 +02:00
Stap 1 vergelijkt elke dependency in ` frontend/` en ` backend/package.json` met
de laatste versie in het npm-register, zodat je bij elke testronde ziet of er
updates open staan. Pakketten die bewust achterblijven staan met reden in
[` docker/update-exceptions.json`](docker/update-exceptions.json) en laten de
check niet falen:
` ``
frontend/package.json (9 dependencies)
[vast ] eslint ^9.39.5 → 10.9.1 eslint-plugin-react ondersteunt nog geen ESLint 10
actueel op de vastgezette versies na
backend/package.json (15 dependencies)
alles actueel
` ``
2026-09-04 11:51:29 +02:00
### Productie release
2026-09-04 09:19:14 +02:00
2026-09-04 11:51:29 +02:00
` ``powershell
2026-09-09 17:36:34 +02:00
# token: staat in .env.release, of eenmalig in de shell
2026-09-10 14:30:18 +02:00
# één Gitea-token met write:package (push) én write:repository (release)
$env:GITEA_TOKEN = "<token met write:package + write:repository>"
2026-09-04 11:51:29 +02:00
.\release.ps1
` ``
Draait de tests, bouwt het app-image voor ` amd64` + ` arm64` (Raspberry Pi),
pusht naar de registry, zet de Git-tag en publiceert de Gitea-release.
2026-09-04 09:19:14 +02:00
2026-09-10 16:24:30 +02:00
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.
2026-09-04 09:19:14 +02:00
---
## 🗂️ Functionaliteit
| Scherm | Wat het doet |
|---|---|
2026-09-11 15:20:09 +02:00
| 📊 **Dashboard** | KPI's over inleg, vermogen, winst en ROI, de voortgang naar je doel, en de grafiek met vermogen tegen inleg |
2026-09-04 09:19:14 +02:00
| 💰 **Eigen vermogen** | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek |
2026-09-11 14:05:55 +02:00
| 📉 **Schuld** | Schulden bijhouden per blok met afbetalingsoverzicht |
| 📈 **Voortgang Schuld & EV** | Grafieken en trends van EV en schuld over tijd (apart bij te houden) |
2026-09-04 09:19:14 +02:00
| 👥 **Gebruikers** | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) |
2026-09-11 14:05:55 +02:00
| ⚙️ **Instellingen** | Licht/donker thema, backup downloaden en backup terugzetten |
2026-09-04 09:19:14 +02:00
2026-09-11 15:20:09 +02:00
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.
2026-09-10 14:03:34 +02:00
De kolommen in de tabellen op Eigen vermogen en Voortgang zijn versleepbaar: pak
de rechterrand van een kolomkop. De breedtes worden per tabel in de browser
bewaard; dubbelklik op de greep voor de standaardbreedte.
2026-09-11 14:05:55 +02:00
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`.
2026-09-04 09:19:14 +02:00
---
## 🏗️ Architectuur
` ``
vibefinance/
2026-09-04 11:51:29 +02:00
├── Dockerfile # Multi-stage: frontend build → app-image
├── .dockerignore
├── docker-compose.yml # Productie: app + postgres
├── docker-compose.dev.yml # Dev: app (API + Vite) + postgres
2026-09-04 09:19:14 +02:00
├── .env.example # Kopieer naar .env en vul in
├── package.json # Versienummer — single source of truth
2026-09-04 11:51:29 +02:00
├── dev.ps1 / dev.sh # Dev script (bouwen, starten, logs, pushen)
├── test.ps1 / test.sh # Testsuite (unit + build + smoketest)
├── release.ps1 / release.sh # Release (test, push, tag, Gitea-release)
│
├── docker/
2026-09-11 14:05:55 +02:00
│ ├── 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
2026-09-04 09:19:14 +02:00
│
2026-09-09 17:36:34 +02:00
├── docs/
│ └── screenshot-dashboard.png # Screenshot bovenaan deze README
│
2026-09-04 11:51:29 +02:00
├── frontend/ # React / Vite SPA (build → dist)
│ ├── vite.config.js # /api proxy naar de API in dezelfde container
2026-09-11 14:05:55 +02:00
│ ├── index.html # Vite-entry, verwijst naar /favicon.svg
│ ├── public/ # Ongebundeld: favicon.svg + icons/
2026-09-04 09:19:14 +02:00
│ └── src/
│ ├── App.jsx # Root – wiring + idle-logout
2026-09-10 15:40:27 +02:00
│ ├── App.css # Reset + losse klassen (o.a. .kolom-grip)
2026-09-04 09:19:14 +02:00
│ ├── main.jsx
2026-09-09 17:36:34 +02:00
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
2026-09-04 09:19:14 +02:00
│ ├── constants/ # Kleuren, tabs, begindata
2026-09-11 14:05:55 +02:00
│ ├── utils/ # format.js (bedragen/datums), storage.js
2026-09-10 14:03:34 +02:00
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
2026-09-11 14:05:55 +02:00
│ ├── context/ # AppContext (global state + API sync + backup)
2026-09-04 09:19:14 +02:00
│ ├── components/
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
│ │ ├── LoginPage.jsx
2026-09-11 14:05:55 +02:00
│ │ ├── LoginBackground.jsx
2026-09-04 09:19:14 +02:00
│ │ ├── NavBar.jsx
│ │ ├── ProfielPopup.jsx
2026-09-11 14:05:55 +02:00
│ │ ├── InstellingenModal.jsx # Thema + backup/restore
2026-09-04 09:19:14 +02:00
│ │ └── GebruikersBeheer.jsx
│ └── pages/
│ ├── DashboardTab.jsx
│ ├── EigenVermogenTab.jsx
│ ├── SchuldTab.jsx
│ └── VoortgangTab.jsx
│
2026-09-04 11:51:29 +02:00
└── backend/ # Express REST API + statische frontend
├── test/ # node --test integratietests
2026-09-04 09:19:14 +02:00
└── src/
2026-09-04 11:51:29 +02:00
├── server.js # Entry point: API op /api, SPA op /
├── db.js # PostgreSQL pool + initDb() schema-migraties
2026-09-11 14:05:55 +02:00
├── logger.js # pino (pino-pretty buiten productie)
2026-09-04 11:51:29 +02:00
├── config/ # Omgevingsvariabelen (incl. STATIC_DIR)
2026-09-04 09:19:14 +02:00
├── constants/ # Begindata (initialData.js)
├── middleware/ # auth.js, errorHandler.js
2026-09-11 14:05:55 +02:00
├── routes/ # auth, data, users, health, update
2026-09-04 09:19:14 +02:00
└── stores/ # userStore, dataStore
` ``
2026-09-04 12:21:06 +02:00
**Stack:** React 19 · Vite 8 · Node.js 22 · Express 5 · PostgreSQL 18 · JWT · bcrypt · Docker Compose v2
2026-09-04 09:19:14 +02:00
2026-09-04 11:51:29 +02:00
### Waarom 2 containers
De frontend werd eerder door een eigen nginx-container geserveerd. Voor
self-hosting op een Raspberry Pi is dat een container te veel: de Express-app
serveert de statische build nu zelf, dus er is geen aparte webserver en geen
interne proxy meer nodig. Dat betekent ook: één origin, dus geen CORS tussen
frontend en API.
| Verzoek | Wat de app doet |
|---|---|
2026-09-11 14:05:55 +02:00
| ` /api/...` | REST API (rate limited, ` Cache-Control: no-store`) |
| ` /assets/...` | Gehashte build-bestanden, 1 jaar ` immutable` gecached |
| ` /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 |
2026-09-04 11:51:29 +02:00
2026-09-04 09:19:14 +02:00
---
## 🔌 API
2026-09-04 11:51:29 +02:00
Beveiligde endpoints vereisen ` Authorization: Bearer <token>`. Rate limiting: 20 req/15 min op auth-routes, 200 req/15 min op de rest van ` /api` (statische bestanden zijn uitgezonderd).
2026-09-04 09:19:14 +02:00
| Route | Methoden | Omschrijving |
|---|---|---|
| ` /api/auth/register` | POST | Account aanmaken (eerste user = Admin) |
| ` /api/auth/login` | POST | Inloggen, JWT token terug |
| ` /api/auth/me` | GET, PATCH | Profiel ophalen en bijwerken |
| ` /api/auth/me/password` | POST | Wachtwoord wijzigen |
| ` /api/data` | GET, PUT, DELETE | Financiële data per gebruiker |
| ` /api/users` | GET, POST, PATCH, DELETE | Gebruikersbeheer (Admin only) |
| ` /api/health` | GET | Status + uptime |
2026-09-04 11:51:29 +02:00
| ` /api/update-check` | GET | Laatste release (voor de update-melding) |
2026-09-04 09:19:14 +02:00
---
## 🗄️ Database
| Tabel | Omschrijving |
|---|---|
| ` users` | Accounts met bcrypt hash, rol, actief-vlag en aanmaakdatum |
| ` user_data` | Financiële data per gebruiker (JSON) |
| Omgeving | Volume | Mount pad |
|---|---|---|
| Productie | ` vibefinance_pgdata` | ` /var/lib/postgresql` |
| Development | ` postgres_dev_data` | ` /var/lib/postgresql` |
> PostgreSQL 18 gebruikt ` PGDATA=/var/lib/postgresql/18/docker` — het volume is daarom gemount op ` /var/lib/postgresql` (niet ` /data`).
2026-09-04 11:51:29 +02:00
Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
` initDb()` in ` backend/src/db.js`.
2026-09-04 09:19:14 +02:00
---
## ⚙️ Omgevingsvariabelen
| Variabele | Standaard | Omschrijving |
|---|---|---|
| ` POSTGRES_PASSWORD` | — | **Verplicht** |
| ` JWT_SECRET` | — | **Verplicht** — minimaal 64 tekens |
| ` POSTGRES_USER` | ` vibefinance` | Database gebruikersnaam |
| ` POSTGRES_DB` | ` vibefinance` | Database naam |
2026-09-11 14:05:55 +02:00
| ` DATABASE_URL` | — | Connectiestring; ` docker-compose.yml` stelt hem samen uit de drie ` POSTGRES_*` waarden, dus zelf zetten hoeft alleen buiten Compose |
2026-09-04 09:19:14 +02:00
| ` JWT_EXPIRES` | ` 7d` | Geldigheidsduur token |
2026-09-04 11:51:29 +02:00
| ` CORS_ORIGIN` | ` http://localhost` | Externe domeinnaam (één origin, dus zelden nodig) |
| ` APP_PORT` | ` 3400` | Poort op de host (container luistert op 3001) |
| ` PORT` | ` 3001` | Poort binnen de container |
| ` STATIC_DIR` | ` /app/public` | Map met de frontend-build |
| ` UPDATE_CHECK_URL` | Gitea releases-API | Bron update-melding (leeg = uit) |
2026-09-10 14:03:34 +02:00
| ` UPDATE_CHECK_TOKEN` | — | Optioneel: de Gitea-repo is publiek, dus de check werkt anoniem. Alleen nodig als de organisatie weer privé wordt; scope ` read:repository` |
2026-09-04 09:19:14 +02:00
---
## 🔐 Beveiliging
- Wachtwoorden gehasht met bcrypt (cost factor 12)
- JWT authenticatie met instelbare vervaltijd
2026-09-11 14:05:55 +02:00
- 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`
2026-09-04 11:51:29 +02:00
- Frontend en API op één origin — geen interne proxy, geen CORS-uitzonderingen
- Content-Security-Policy via Helmet, toegespitst op wat de app laadt
2026-09-04 09:19:14 +02:00
- Database bereikbaar alleen binnen het interne Docker netwerk
2026-09-04 11:51:29 +02:00
- App-container draait als non-root
- Rate limiting op ` /api` (statische bestanden uitgezonderd)
---
## 🧪 Testen
| Laag | Wat | Commando |
|---|---|---|
2026-09-04 12:24:22 +02:00
| Dependencies | Staan ` package.json`'s op de laatste versies | ` .\test.ps1 -Unit` |
2026-09-04 11:51:29 +02:00
| Backend | API-routes, SPA-fallback, cache- en security-headers | ` .\test.ps1 -Unit` |
| Build | Vite-build + productie-image | ` .\test.ps1 -NoSmoke` |
| Integratie | Smoketest op de echte app + postgres stack | ` .\test.ps1` |
2026-09-04 09:19:14 +02:00
---
## 🛠️ Beheer
2026-09-09 17:36:34 +02:00
Zie ` BEHEER.md` voor commando's voor lokale ontwikkeling, releases, logs en
2026-09-10 14:30:18 +02:00
database-toegang — en voor de sectie **Tokens** : waarom één Gitea-token op het
werkstation genoeg is, welke scopes het nodig heeft, en wat een 401, 403 of 404
van Gitea betekent.
2026-09-04 09:19:14 +02:00
---
<div align="center">
Privé gebruik · gebouwd met [Claude.ai ](https://claude.ai )
</div>