Files
VibeFinance/README.md
T

15 KiB
Raw Permalink Blame History

💜 VibeFinance

Persoonlijk vermogensdashboard

Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer

React Node.js PostgreSQL Docker Version Built with Claude

VibeFinance dashboard

🤖 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 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:

    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 up laat de dev-database staan; alleen .\dev.ps1 down wist de volumes.

Testen

.\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)

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

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 Grafieken en trends van EV en schuld over tijd (apart bij te houden)
👥 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 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.


🏗️ 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/          # Kleuren, tabs, begindata
│       ├── utils/              # format.js (bedragen/datums), storage.js
│       ├── 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
│       │   └── 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/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
  • 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