20 KiB
💜 VibeFinance
Persoonlijk vermogensdashboard
Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer
🤖 Gebouwd met Claude.ai
Deze applicatie is volledig ontworpen en ontwikkeld met behulp van 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+
De app draait in 2 containers: app (Express API + gebouwde frontend) en
postgres (database).
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:
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:
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:
POSTGRES_USER=vibefinance
POSTGRES_PASSWORD=kies_een_sterk_wachtwoord
JWT_SECRET=minimaal_64_willekeurige_tekens
APP_PORT=3400
# Genereer een veilige JWT_SECRET
openssl rand -hex 64
3. Starten:
cp .env.example .env # en vul de waarden in
docker compose pull && docker compose up -d
Twee foutmeldingen en wat ze betekenen:
-
no configuration file provided: not found— je staat in een map zonderdocker-compose.yml. Controleer metls docker-compose.yml, of geef het bestand expliciet mee:docker compose -f /pad/naar/docker-compose.yml up -d. -
401 Unauthorizedof "pull access denied" — de organisatie staat niet meer op publiek. Dan alsnog inloggen:docker login vibehoogie.duckdns.org # wachtwoord = Gitea-token met read:package
Navigeer naar http://<server-ip>:3400 en registreer je account. De eerste geregistreerde gebruiker krijgt automatisch de Admin-rol.
De container luistert intern op poort 3001 en serveert daar zowel de frontend
(op /) als de API (op /api). APP_PORT bepaalt de poort op de host.
Development (hot reload)
.\dev.ps1
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:
.\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 uplaat de dev-database staan; alleen.\dev.ps1 downwist de volumes.
Testen
.\test.ps1 # deps + backend-tests + build + imagescan + smoketest
.\test.ps1 -Unit # dependency-check + backend-tests
.\test.ps1 -NoSmoke # zonder de stack te starten (imagescan draait wél)
.\test.ps1 -Strict # faal op achterlopende deps; scan ook op MEDIUM/LOW
.\test.ps1 -SkipDeps # dependency-check overslaan (bijv. offline)
.\test.ps1 -SkipScan # imagescan overslaan (bijv. offline)
Alles loopt in Docker — Node op de host is niet nodig. De backend-tests draaien
zonder database via het test-target in de Dockerfile.
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 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
Stap 4 scant het gebouwde image met Trivy en blokkeert bij
CRITICAL of HIGH waarvoor een fix bestaat. Dat is iets anders dan stap 1: die
kijkt naar de package.json's, deze naar wat er daadwerkelijk in het image zit
— de Alpine-pakketten en alles wat het base-image meelevert. Dat onderscheid is
niet theoretisch. Bij de scan van 1.0.10 kwamen alle 39 bevindingen daarvandaan
en geen enkele uit de applicatie zelf:
| Bron | Aantal | Waar het vandaan kwam |
|---|---|---|
libssl3 + libcrypto3 |
20 | Alpine in het node-image liep achter op 3.5.8-r0 |
| npm's eigen dependencies | 19 | tar, pacote, sigstore, brace-expansion, picomatch, ip-address |
Een dependency-check had er nul van gezien. Het productie-image doet daarom aan
het eind van de build een apk upgrade en gooit npm, npx, corepack en yarn
eruit: de container start met node src/server.js en gebruikt nooit een
pakketbeheerder. Dat bracht de scan op nul, en houdt hem daar — npm's
dependencies zijn een terugkerende bron die nu simpelweg niet meer in het image
zit. release.ps1 draait dezelfde scan vóór het pushen, dus een image met
openstaande CVE's komt de registry niet in.
Productie release
# token: staat in .env.release, of eenmalig in de shell
# één Gitea-token met write:package (push) én write:repository (release)
$env:GITEA_TOKEN = "<token met write:package + write:repository>"
.\release.ps1
Draait de tests, bouwt het app-image voor amd64 + arm64 (Raspberry Pi),
pusht naar de registry, zet de Git-tag en publiceert de Gitea-release.
Het script regelt ook het bijwerken eromheen: de changelog-kop
## [Niet vrijgegeven] wordt ## [x.x.x] - datum, de versie in
frontend/package.json en de badge hierboven gaan mee, en ná het publiceren
zet het de root package.json op x.x.(x+1)-dev voor de volgende cyclus.
Beschrijft de changelog niets over deze versie, dan stopt het script vóór de
build — anders zou de release met een placeholder als notes online komen.
🗂️ Functionaliteit
| 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 |
| 💰 Eigen vermogen | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek |
| 📉 Schuld | Schulden bijhouden per blok met afbetalingsoverzicht |
| 📈 Voortgang Schuld & EV | KPI-kaarten met de verdeling van schuld en EV, het rendement en de netto waarde, plus grafieken en trends van EV en schuld over tijd (apart bij te houden) |
| 👥 Gebruikers | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) |
| ⚙️ Instellingen | Kleurthema, licht/donker, backup downloaden, backup terugzetten en export naar HTML |
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.
Rendement betekent overal hetzelfde: huidige waarde min geïnvesteerde fiat,
gedeeld door die inleg. De kaart "Rendement Eigen Vermogen" op Voortgang en het
blok "Rendement" op Eigen vermogen tonen daarom hetzelfde totaal — dat laatste
splitst het alleen nog uit per categorie. Zolang er geen inleg is vastgelegd
staat er — in plaats van een percentage, want zonder noemer is er geen
rendement te berekenen.
De x-as noemt maanden (sep '26), niet losse datums: de grafiek rekent uit
hoeveel labels er naast elkaar passen en toont er zoveel, terugtellend vanaf het
laatste punt zodat dat er altijd bij staat. Bij een langere reeks dunt hij
vanzelf verder uit. De exacte dag lees je in de tooltip. De vermogenslijn is een
Catmull-Rom spline; de inleglijn krijgt bewust rechte segmenten, omdat een
spline tussen twee punten iets buiten hun bereik uitschiet en dat bij een reeks
die nooit terugloopt een daling zou tekenen die er niet is. Beide lijnen delen
de tekenhulp in
frontend/src/utils/grafiek.js.
Onder Instellingen kies je een van de negen kleurthema's. Thema en
licht/donker staan los van elkaar: het thema levert het accent, de schakelaar de
ondergrond. De paletten staan in
frontend/src/constants/themas.js;
useTheme(themaId, darkMode) leidt daar randen, invoervelden en schaduwen uit af.
Groen, rood en goud veranderen bewust nooit mee — die betekenen winst, verlies en
inleg, en betekenis hoort niet van een kleurkeuze af te hangen. Thema en modus
worden per browser bewaard.
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.
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.
Daarnaast staat er "Exporteren naar HTML": één zelfdragend bestand met al je gegevens, bedoeld als laatste vangnet. Draait de app niet meer, is de database weg en ligt er geen backup, dan opent dit bestand nog steeds — in elke browser, op een computer zonder internet. Er zit niets van buiten in: de opmaak staat inline, er worden geen lettertypen, afbeeldingen of scripts geladen. Je ziet een samenvatting met de KPI's, eigen vermogen en schulden per categorie met subtotalen, de initiële schuld met wat er sinds de start is afgelost, de voortgangstabel en de historie.
Alles wat je ooit hebt ingevoerd komt mee, ook wat de app zelf niet meer toont:
stortingen op een inmiddels verwijderde categorie krijgen een eigen kolom
(Verwijderd (c9)). De voortgangstabel is bij zes categorieën twaalf kolommen
breed en scrollt horizontaal in plaats van kolommen af te kappen; de datumkolom
blijft daarbij staan. Op print vervalt de scrollbalk en wordt de tabel kleiner
gezet, zodat alle kolommen op papier alsnog passen.
Het bestand is tegelijk een backup: onderin staat de complete ruwe data in een
<script type="application/json">-blok dat de browser niet uitvoert en niet
toont. "Backup herstellen" accepteert daarom naast de ZIP ook zo'n HTML-bestand.
Eén bestand dat je zowel kunt lézen als kunt terugzetten — zie
frontend/src/utils/htmlExport.js.
🏗️ Architectuur
vibefinance/
├── Dockerfile # Multi-stage: frontend build → app-image
├── .dockerignore
├── docker-compose.yml # Productie: app + postgres
├── docker-compose.dev.yml # Dev: app (API + Vite) + postgres
├── .env.example # Kopieer naar .env en vul in
├── package.json # Versienummer — single source of truth
├── 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/
│ ├── 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/
│ └── screenshot-dashboard.png # Screenshot bovenaan deze README
│
├── frontend/ # React / Vite SPA (build → dist)
│ ├── 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/
│ ├── App.jsx # Root – wiring + idle-logout
│ ├── App.css # Reset + losse klassen (o.a. .kolom-grip)
│ ├── main.jsx
│ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build)
│ ├── constants/ # index.js (kleuren, tabs, begindata), themas.js
│ ├── utils/ # format.js (bedragen/datums), storage.js,
│ │ # grafiek.js (SVG), htmlExport.js (export)
│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes
│ ├── context/ # AppContext (global state + API sync + backup)
│ ├── components/
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
│ │ ├── LoginPage.jsx
│ │ ├── LoginBackground.jsx
│ │ ├── NavBar.jsx
│ │ ├── ProfielPopup.jsx
│ │ ├── InstellingenModal.jsx # Thema + backup/restore + export
│ │ └── GebruikersBeheer.jsx
│ └── pages/
│ ├── DashboardTab.jsx
│ ├── EigenVermogenTab.jsx
│ ├── SchuldTab.jsx
│ └── VoortgangTab.jsx
│
└── backend/ # Express REST API + statische frontend
├── test/ # node --test integratietests
└── src/
├── server.js # Entry point: API op /api, SPA op /
├── db.js # PostgreSQL pool + initDb() schema-migraties
├── logger.js # pino (pino-pretty buiten productie)
├── config/ # Omgevingsvariabelen (incl. STATIC_DIR)
├── constants/ # Begindata (initialData.js)
├── middleware/ # auth.js, errorHandler.js
├── routes/ # auth, data, users, health, update
└── stores/ # userStore, dataStore
Stack: React 19 · Vite 8 · Node.js 22 · Express 5 · PostgreSQL 18 · JWT · bcrypt · Docker Compose v2
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 |
|---|---|
/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 |
🔌 API
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).
| 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 |
/api/update-check |
GET | Laatste release (voor de update-melding) |
🗄️ 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).
Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
initDb() in backend/src/db.js.
⚙️ Omgevingsvariabelen
| Variabele | Standaard | Omschrijving |
|---|---|---|
POSTGRES_PASSWORD |
— | Verplicht |
JWT_SECRET |
— | Verplicht — minimaal 64 tekens |
POSTGRES_USER |
vibefinance |
Database gebruikersnaam |
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 |
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) |
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 |
🔐 Beveiliging
- Wachtwoorden gehasht met bcrypt (cost factor 12)
- JWT authenticatie met instelbare vervaltijd
- Data is per gebruiker:
/api/dataleest en schrijft alleen de rij van de ingelogde gebruiker (subuit het token) - Van de rollen wordt alleen Admin afgedwongen, op
/api/users— zie de sectie Rollen inBEHEER.md - Frontend en API op één origin — geen interne proxy, geen CORS-uitzonderingen
- Content-Security-Policy via Helmet, toegespitst op wat de app laadt
- Database bereikbaar alleen binnen het interne Docker netwerk
- App-container draait als non-root
- Rate limiting op
/api(statische bestanden uitgezonderd)
🧪 Testen
| Laag | Wat | Commando |
|---|---|---|
| Dependencies | Staan package.json's op de laatste versies |
.\test.ps1 -Unit |
| 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 |
🛠️ Beheer
Zie BEHEER.md voor commando's voor lokale ontwikkeling, releases, logs en
database-toegang — en voor de sectie Tokens: waarom één Gitea-token op het
werkstation genoeg is, welke scopes het nodig heeft, en wat een 401, 403 of 404
van Gitea betekent.
Privé gebruik · gebouwd met Claude.ai