diff --git a/BEHEER.md b/BEHEER.md index 164a5fd..5a82fb9 100644 --- a/BEHEER.md +++ b/BEHEER.md @@ -184,11 +184,48 @@ docker login vibehoogie.duckdns.org:3000 ## Update-notificaties -De app vergelijkt de draaiende versie met de laatste release op Gitea. Configuratie staat in `package.json`: +De app vergelijkt de draaiende versie met de laatste **release** op Gitea en toont +dat op het belletje in de topbalk en onder "Over VibeFinance". + +**Hoe de check loopt** + +1. De frontend vraagt `/api/update-check` op de eigen backend — de Gitea API stuurt + geen CORS-headers, dus de browser mag die niet direct bevragen. +2. De backend haalt `releases/latest` bij Gitea op en cachet het antwoord 15 minuten. +3. De frontend kijkt bij het openen, daarna elk half uur en bij het terugkeren naar + het tabblad. Alleen een écht hoger versienummer geeft een melding. + +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` +in de URL. + +**Configuratie** + +De bron staat in de root `package.json`: ```json -"repository": "https://vibehoogie.duckdns.org:3000/vibe/VibeFinance", -"updateCheckUrl": "https://vibehoogie.duckdns.org:3000/api/v1/repos/vibe/VibeFinance/releases/latest" +"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance", +"updateCheckUrl": "https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest" ``` -Vite en de build-scripts lezen deze velden automatisch uit — geen handmatige build-args nodig. +`dev.ps1` zet die URL als `UPDATE_CHECK_URL` in de omgeving; in productie komt hij +uit `.env` (of de default in `docker-compose.yml`). Beschikbare backend-variabelen: + +| Variabele | Betekenis | +|---|---| +| `UPDATE_CHECK_URL` | Release-API die gepolld wordt. Leeg = melding uit. | +| `UPDATE_CHECK_TOKEN` | Alleen nodig als de repo in Gitea niet publiek leesbaar is. | + +**Een release publiceren** + +Een kale Git-tag is niet genoeg: `releases/latest` geeft alleen echte releases terug. +`release.ps1` maakt die aan (stap 4) met de changelog-sectie van die versie als +release-notes. Daarvoor is een Gitea-token met release-rechten nodig: + +```powershell +$env:GITEA_TOKEN = "" +.\release.ps1 +``` + +Zonder token slaat het script die stap over met een waarschuwing — images en tag +staan dan wel klaar, maar gebruikers zien geen melding tot de release bestaat. diff --git a/Changelog.md b/Changelog.md index d4c9b56..bc25065 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,27 @@ # Changelog +## [0.1.5] - 2026-09-04 + +### Toegevoegd +- Donut-grafieken: hover per categorie toont naam, bedrag en percentage + +### Gewijzigd +- Update-check loopt nu via de eigen backend (`/api/update-check`) in plaats van + rechtstreeks naar de Gitea API — de browser blokkeerde die call (geen CORS-headers) +- Versievergelijking is semver-based: alleen een hoger versienummer meldt, `-dev` + builds melden niets meer +- Update-check herhaalt zich elk half uur en bij terugkeer naar het tabblad + (was: eenmalig bij laden) +- `release.ps1` maakt de Gitea-release aan met de changelog-sectie als release-notes + (een kale Git-tag komt niet in `releases/latest` en gaf dus geen melding) +- Middenwaarde in de donut schaalt mee met de beschikbare ruimte + +### Opgelost +- Donkere spleten tussen de donut-segmenten (de tussenruimte liet de achtergrondbaan zien) +- De update-melding op het belletje bleef altijd leeg door de geblokkeerde API-call + +--- + ## [0.1.3] - 2026-04-16 ### Toegevoegd diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 6b90ae6..20f0568 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -8,4 +8,11 @@ export const config = { JWT_EXPIRES: process.env.JWT_EXPIRES || "7d", DATABASE_URL: process.env.DATABASE_URL, REDIS_URL: process.env.REDIS_URL, + + // Update-notificaties: welke release-API wordt gepolld (Gitea). + // Leeg laten schakelt de melding in de app uit. + UPDATE_CHECK_URL: process.env.UPDATE_CHECK_URL + ?? "https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest", + // Alleen nodig wanneer de repo in Gitea niet publiek leesbaar is. + UPDATE_CHECK_TOKEN: process.env.UPDATE_CHECK_TOKEN || null, }; diff --git a/backend/src/routes/update.js b/backend/src/routes/update.js new file mode 100644 index 0000000..dc3d9d5 --- /dev/null +++ b/backend/src/routes/update.js @@ -0,0 +1,52 @@ +import { Router } from "express"; +import { config } from "../config/index.js"; +import { logger } from "../logger.js"; + +const router = Router(); + +// Gitea wordt server-side bevraagd: de browser mag dat niet (geen CORS-headers +// op de Gitea API) en zo blijft het antwoord gecached voor alle gebruikers. +const TTL_MS = 15 * 60 * 1000; +const TIMEOUT_MS = 5000; +let cache = { at: 0, payload: null }; + +router.get("/", async (_req, res) => { + if (!config.UPDATE_CHECK_URL) { + return res.json({ latest: null, disabled: true }); + } + if (cache.payload && Date.now() - cache.at < TTL_MS) { + return res.json(cache.payload); + } + + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS); + try { + const upstream = await fetch(config.UPDATE_CHECK_URL, { + signal: ctrl.signal, + headers: { + Accept: "application/json", + ...(config.UPDATE_CHECK_TOKEN && { Authorization: `token ${config.UPDATE_CHECK_TOKEN}` }), + }, + }); + if (!upstream.ok) throw new Error(`upstream ${upstream.status}`); + const rel = await upstream.json(); + + const payload = { + latest: String(rel?.tag_name ?? "").replace(/^v/, "") || null, + naam: rel?.name || null, + notes: rel?.body || null, + url: rel?.html_url || null, + publishedAt: rel?.published_at || null, + }; + cache = { at: Date.now(), payload }; + res.json(payload); + } catch (err) { + logger.warn({ err: err.message }, "Update-check bij Gitea mislukt"); + // Val terug op het laatst bekende antwoord; anders "geen update bekend". + res.json(cache.payload ?? { latest: null, error: true }); + } finally { + clearTimeout(timer); + } +}); + +export default router; diff --git a/backend/src/server.js b/backend/src/server.js index 40a21af..4e0570b 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -10,6 +10,7 @@ import authRouter from "./routes/auth.js"; import dataRouter from "./routes/data.js"; import usersRouter from "./routes/users.js"; import healthRouter from "./routes/health.js"; +import updateRouter from "./routes/update.js"; import { errorHandler } from "./middleware/errorHandler.js"; export const logger = pino({ @@ -41,6 +42,7 @@ if (config.NODE_ENV !== "test") { // ── Routes ──────────────────────────────────────────────────────────────────── app.use("/api/health", healthRouter); +app.use("/api/update-check", updateRouter); app.use("/api/auth", authRouter); app.use("/api/data", dataRouter); app.use("/api/users", usersRouter); diff --git a/dev.ps1 b/dev.ps1 index a2fc993..db38a7c 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -54,15 +54,15 @@ if (-not $appVersion) { $env:VERSION = $appVersion $env:VITE_APP_VERSION = $appVersion +# De backend doet de update-check (server-side, geen CORS) — URL doorgeven aan compose +$env:UPDATE_CHECK_URL = $updateCheckUrl if (-not $env:POSTGRES_PASSWORD) { $env:POSTGRES_PASSWORD = "devpassword" } # Versie synchroniseren naar frontend/package.json $frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json" $frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json -$updateUrl = if ($rootPkg.updateCheckUrl) { $rootPkg.updateCheckUrl } else { "" } -if ($frontendPkg.version -ne $appVersion -or $frontendPkg.updateCheckUrl -ne $updateUrl) { +if ($frontendPkg.version -ne $appVersion) { $frontendPkg.version = $appVersion - $frontendPkg.updateCheckUrl = $updateUrl [System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10)) Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green } @@ -113,7 +113,6 @@ function Invoke-FrontendPush { --build-arg APP_VERSION=$appVersion ` --build-arg VITE_API_URL=$apiUrl ` --build-arg VITE_REPO_URL=$repoUrl ` - --build-arg VITE_UPDATE_CHECK_URL=$updateCheckUrl ` (Join-Path $PSScriptRoot "frontend") if ($LASTEXITCODE -ne 0) { throw "Frontend build mislukt" } Write-Host "Gepusht: $frontendVersionDev" -ForegroundColor Green diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 509c82a..8daca4e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -67,6 +67,7 @@ services: JWT_SECRET: dev-secret-change-in-production JWT_EXPIRES: 7d CORS_ORIGIN: http://localhost + UPDATE_CHECK_URL: ${UPDATE_CHECK_URL:-https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest} DATABASE_URL: postgresql://${POSTGRES_USER:-vibefinance}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/${POSTGRES_DB:-vibefinance} networks: - dev diff --git a/docker-compose.yml b/docker-compose.yml index 20fd3ad..652f67f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,8 @@ services: env_file: .env environment: DATABASE_URL: postgresql://${POSTGRES_USER:-vibefinance}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-vibefinance} + # Bron voor de update-melding in de app (leeg = melding uit) + UPDATE_CHECK_URL: ${UPDATE_CHECK_URL:-https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest} healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3001/api/health | grep -q status || exit 1"] interval: 10s diff --git a/frontend/package.json b/frontend/package.json index 0a6e51c..4458a39 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,6 @@ { "name": "vibefinance-frontend", "version": "0.1.5-dev", - "updateCheckUrl": "https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest", "private": true, "type": "module", "scripts": { diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index d57f17b..c940130 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1,8 +1,8 @@ - - + + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 478170c..745cef7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,7 +10,8 @@ import SchuldTab from "./pages/SchuldTab.jsx"; import VoortgangTab from "./pages/VoortgangTab.jsx"; import { RED, PURPLE_LIGHT } from "./constants/index.js"; import { fmt } from "./utils/format.js"; -import { version, updateCheckUrl } from "../package.json"; +import { version } from "../package.json"; +import { useUpdateCheck } from "./hooks/useUpdateCheck.js"; import ProfielPopup from "./components/ProfielPopup.jsx"; import InstellingenModal from "./components/InstellingenModal.jsx"; @@ -26,21 +27,10 @@ function AppInner() { const { loggedIn, login, logout, locked, tab, showUsers, setShowUsers, showInstellingen, setShowInstellingen, loading, T, currentUser, avatar, idleMinutes, totEV } = useApp(); const [profielOpen, setProfielOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); - const [nieuweVersie, setNieuweVersie] = useState(null); + const nieuweVersie = useUpdateCheck(); // { latest, naam, notes, url } of null const [bellOpen, setBellOpen] = useState(false); const searchRef = useRef(null); - useEffect(() => { - if (!updateCheckUrl) return; - fetch(updateCheckUrl) - .then(r => r.json()) - .then(data => { - const latest = data?.tag_name?.replace(/^v/, ""); - if (latest && latest !== version) setNieuweVersie(latest); - }) - .catch(() => {}); - }, []); - const idleTimer = useRef(null); const IDLE_MS = (idleMinutes ?? 30) * 60 * 1000; const [sessionVerlopen, setSessionVerlopen] = useState(false); @@ -190,7 +180,7 @@ function AppInner() {
@@ -217,8 +207,24 @@ function AppInner() { Nieuwe versie beschikbaar
- v{nieuweVersie} is uitgebracht. Je gebruikt momenteel v{version}. + v{nieuweVersie.latest} is uitgebracht. Je gebruikt momenteel v{version}.
+ {nieuweVersie.notes && ( +
+ {nieuweVersie.notes} +
+ )} + {nieuweVersie.url && ( + + Release bekijken → + + )} diff --git a/frontend/src/components/GebruikersBeheer.jsx b/frontend/src/components/GebruikersBeheer.jsx index c6ffdb2..37ac33b 100644 --- a/frontend/src/components/GebruikersBeheer.jsx +++ b/frontend/src/components/GebruikersBeheer.jsx @@ -183,7 +183,7 @@ export default function GebruikersBeheer({ onClose }) {
e.stopPropagation()} style={{ - background: T.card, border: `1px solid ${T.border}`, + background: T.popupBg, border: `1px solid ${T.border}`, borderRadius: 16, width: "100%", maxWidth: 700, maxHeight: "90vh", display: "flex", flexDirection: "column", boxShadow: "0 16px 48px rgba(0,0,0,0.4)", diff --git a/frontend/src/components/InstellingenModal.jsx b/frontend/src/components/InstellingenModal.jsx index 004a2f8..e1423ab 100644 --- a/frontend/src/components/InstellingenModal.jsx +++ b/frontend/src/components/InstellingenModal.jsx @@ -49,7 +49,7 @@ export default function InstellingenModal({ onClose }) {
e.stopPropagation()} style={{ - background: T.card, border: `1px solid ${T.border}`, + background: T.popupBg, border: `1px solid ${T.border}`, borderRadius: 16, padding: "22px 22px 8px", width: 360, boxShadow: "0 16px 48px rgba(0,0,0,0.4)", color: T.text, diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx index b56a5a8..ff6b0cf 100644 --- a/frontend/src/components/LoginPage.jsx +++ b/frontend/src/components/LoginPage.jsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { RED } from "../constants/index.js"; +import LoginBackground from "./LoginBackground.jsx"; const MailIcon = () => ( @@ -43,16 +44,17 @@ function FloatingInput({ label, type = "text", value, onChange, onKeyDown, icon, border: `1px solid ${focused ? "rgba(124,58,237,0.7)" : "rgba(255,255,255,0.1)"}`, borderRadius: 10, background: "rgba(255,255,255,0.03)", transition: "border-color 0.15s", + padding: "8px 14px 4px", }}> -
+
{icon && ( {icon} @@ -74,27 +76,6 @@ function FloatingInput({ label, type = "text", value, onChange, onKeyDown, icon, ); } -function MiniBarChart() { - const bars = [38, 52, 45, 68, 74, 61, 89]; - const months = ["nov", "dec", "jan", "feb", "mrt", "apr", "mei"]; - return ( -
- {bars.map((h, i) => ( -
-
- {months[i]} -
- ))} -
- ); -} - export default function LoginPage({ onLogin }) { const [mode, setMode] = useState("login"); const [naam, setNaam] = useState(""); @@ -153,7 +134,97 @@ export default function LoginPage({ onLogin }) { boxShadow: "0 40px 100px rgba(0,0,0,0.75), 0 0 0 1px rgba(255,255,255,0.06)", }}> - {/* LEFT: Form panel */} + {/* LEFT: Merkeigen achtergrond + floating light app-screens */} +
+ + + {/* Logo top-left */} +
+
+ + + + +
+ VibeFinance +
+ + {/* Floating light "app screen" card */} +
+
+
+
Vermogensoverzicht
+
The Road naar € 500.000
+
+
Mei 2026
+
+ {[ + { label: "Eigen Vermogen", val: "€ 445.500", color: "#34d399", icon: "↑" }, + { label: "Schuld", val: "€ 54.500", color: "#fb923c", icon: "↓" }, + { label: "Winst", val: "€ 142.500", color: "#c084fc", icon: "↑" }, + ].map(({ label, val, color, icon }) => ( +
+
+
{icon}
+ {label} +
+ {val} +
+ ))} +
+
+ Voortgang naar doel + 89% +
+
+
+
+
+
+ + {/* Floating PNL badge — overlapt het screen, zoals het voorbeeld */} +
+
+ + + +
+
+
ROI
+
+47,03%
+
+
+ + {/* Bottom tagline */} +
+ Beheer je vermogen, volg je groei en bereik je financiële doelen — allemaal op één plek. +
+
+ + {/* RIGHT: Form panel */}
- {/* RIGHT: Purple mockup panel */} -
- {/* Decorative circles */} -
-
-
-
- - {/* Top badge */} -
- ✦ - Jouw vermogen in één oogopslag -
- - {/* Main glassmorphism card */} -
- {/* Card header */} -
-
-
Vermogensoverzicht
-
The Road naar € 500.000
-
-
Mei 2026
-
- - {/* KPI rows */} - {[ - { label: "Eigen Vermogen", val: "€ 445.500", color: "#10b981", icon: "↑" }, - { label: "Schuld", val: "€ 54.500", color: "#f97316", icon: "↓" }, - { label: "Winst", val: "€ 142.500", color: "#a855f7", icon: "↑" }, - ].map(({ label, val, color, icon }) => ( -
-
-
{icon}
- {label} -
- {val} -
- ))} - - {/* Progress toward goal */} -
-
- Voortgang naar doel - 89% -
-
-
-
-
- € 0 - € 500.000 -
-
- - {/* Mini bar chart */} -
-
Groei per maand
- -
-
- - {/* Floating badges */} -
-
-
ROI
-
+47,03%
-
-
-
Doel
-
€ 500.000
-
-
- - {/* Bottom tagline */} -
- Beheer je vermogen, volg je groei en bereik je financiële doelen — allemaal op één plek. -
-
-
); diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx index f636630..6b3bdcb 100644 --- a/frontend/src/components/NavBar.jsx +++ b/frontend/src/components/NavBar.jsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { TABS } from "../constants/index.js"; -import { version, updateCheckUrl } from "../../package.json"; +import { version } from "../../package.json"; +import { useUpdateCheck } from "../hooks/useUpdateCheck.js"; import { useApp } from "../context/AppContext.jsx"; const TAB_ICONS = [ @@ -123,7 +124,7 @@ function OverPopup({ onClose, nieuweVersie, T }) { }}> {/* Modal */}
e.stopPropagation()} style={{ - background: T.card, + background: T.popupBg, border: `1px solid ${T.border}`, borderRadius: 16, padding: "28px 28px 24px", width: 300, boxShadow: "0 24px 60px rgba(0,0,0,0.5)", @@ -166,7 +167,7 @@ function OverPopup({ onClose, nieuweVersie, T }) {
Nieuw beschikbaar - v{nieuweVersie} + v{nieuweVersie.latest}
)} @@ -213,20 +214,9 @@ function DarkModeToggle({ darkMode, setDarkMode, T }) { export default function NavBar() { const { tab, setTab, setShowUsers, setShowInstellingen, T, logout, darkMode, setDarkMode, currentUser, avatar } = useApp(); - const [nieuweVersie, setNieuweVersie] = useState(null); + const nieuweVersie = useUpdateCheck(); const [overOpen, setOverOpen] = useState(false); - useEffect(() => { - if (!updateCheckUrl) return; - fetch(updateCheckUrl) - .then(r => r.json()) - .then(data => { - const latest = data?.tag_name?.replace(/^v/, ""); - if (latest && latest !== version) setNieuweVersie(latest); - }) - .catch(() => {}); - }, []); - return (
s.val > 0); - const GAP = active.length > 1 ? circ * 0.022 : 0; + const multi = active.length > 1; + + // Middenwaarde schaalt mee met de beschikbare ruimte binnen de ring, zodat + // lange bedragen niet uit verhouding raken. + const innerW = (2 * r - strokeW) * 0.92; + const valLen = centerVal ? String(centerVal).length : 0; + const valFont = valLen + ? Math.min(size * 0.125, innerW / (valLen * 0.6)) + : 0; + + // Segmenten sluiten naadloos aan (geen tussenruimte). let off = 0; const slices = active.map((s) => { const raw = (s.val / total) * circ; - const dash = Math.max(raw - GAP, 2); - const sl = { ...s, dash, offset: off }; + const sl = { ...s, dash: Math.max(raw, 1), offset: off, share: total ? s.val / total : 0 }; off += raw; return sl; }); - const glowId = `dgl${size}`; + + const track = (i) => (e) => { + const box = wrapRef.current?.getBoundingClientRect(); + setHover({ + i, + x: box ? e.clientX - box.left : cx, + y: box ? e.clientY - box.top : cy, + }); + }; + const hovered = hover ? slices[hover.i] : null; + const fmtPct = (v) => `${(v * 100).toFixed(1).replace(".", ",")}%`; + const fmtEur = (v) => + new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(v || 0); + return ( - - - - - - +
+ + {/* Baan — zelfde toon als de voortgangsbalken */} + - {/* Dark inner fill */} - + {/* Segmenten */} + {slices.map((s, i) => ( + setHover(null)} + /> + ))} - {/* Track */} - + {/* Center label */} + {label && ( + + {label} + + )} + {/* Center value */} + {centerVal && ( + + {centerVal} + + )} + - {/* Glow pass */} - {slices.map((s, i) => ( - - ))} - - {/* Crisp segments */} - {slices.map((s, i) => ( - - ))} - - {/* Center label */} - {label && ( - - {label} - + {/* Tooltip: categorie, bedrag en aandeel */} + {hovered && ( +
+
+ + + {hovered.naam || hovered.label || "—"} + +
+
+ {fmtEur(hovered.val)} · {fmtPct(hovered.share)} +
+
)} - {/* Center value */} - {centerVal && ( - - {centerVal} - - )} - +
); } @@ -289,6 +324,7 @@ export function Overlay({ children, onClose, zIndex = 400 }) { } export function ModalBox({ children, maxWidth = 480 }) { + const { T } = useApp(); const ref = useRef(null); const [pos, setPos] = useState(null); const dragState = useRef(null); @@ -331,8 +367,8 @@ export function ModalBox({ children, maxWidth = 480 }) {
{ + const [core, pre] = String(v ?? "").replace(/^v/, "").split("-"); + const n = core.split(".").map((x) => parseInt(x, 10) || 0); + return { n: [n[0] ?? 0, n[1] ?? 0, n[2] ?? 0], pre: pre || null }; + }; + const a = parse(latest), b = parse(huidig); + for (let i = 0; i < 3; i++) { + if (a.n[i] !== b.n[i]) return a.n[i] > b.n[i]; + } + return !a.pre && !!b.pre; +} + +/** + * Geeft de nieuwste release terug zodra die nieuwer is dan de draaiende versie, + * anders `null`. Vorm: { latest, naam, notes, url, publishedAt }. + */ +export function useUpdateCheck() { + const [update, setUpdate] = useState(null); + + useEffect(() => { + // Dev-builds lopen per definitie voor op de laatste release — geen melding. + // Met ?updatecheck in de URL forceer je de check alsnog (voor testen). + const forceer = new URLSearchParams(window.location.search).has("updatecheck"); + if (version.includes("-") && !forceer) return; + + let gestopt = false; + let laatste = 0; + + const check = async () => { + if (gestopt || Date.now() - laatste < MIN_GAP_MS) return; + laatste = Date.now(); + try { + const res = await fetch("/api/update-check"); + if (!res.ok || gestopt) return; + const rel = await res.json(); + if (gestopt) return; + setUpdate(rel?.latest && isNieuwer(rel.latest, version) ? rel : null); + } catch { + // Backend onbereikbaar of offline: gewoon geen melding tonen. + } + }; + + check(); + const iv = setInterval(check, POLL_MS); + window.addEventListener("focus", check); + return () => { + gestopt = true; + clearInterval(iv); + window.removeEventListener("focus", check); + }; + }, []); + + return update; +} diff --git a/frontend/src/pages/EigenVermogenTab.jsx b/frontend/src/pages/EigenVermogenTab.jsx index 8f27bcd..fc5acdf 100644 --- a/frontend/src/pages/EigenVermogenTab.jsx +++ b/frontend/src/pages/EigenVermogenTab.jsx @@ -539,7 +539,7 @@ export default function EigenVermogenTab() { flex: 1, minWidth: 200, display: "flex", flexDirection: "column", alignItems: "center", }}> - ({ val: c.tot, color: c.color }))} total={totVerdeling} size={200} label="Verdeling" centerVal={fmt(totVerdeling)} /> + ({ val: c.tot, color: c.color, naam: c.naam }))} total={totVerdeling} size={200} label="Verdeling" centerVal={fmt(totVerdeling)} />
{catTotals.map((c) => (
diff --git a/frontend/src/pages/SchuldTab.jsx b/frontend/src/pages/SchuldTab.jsx index 5f05223..962c518 100644 --- a/frontend/src/pages/SchuldTab.jsx +++ b/frontend/src/pages/SchuldTab.jsx @@ -375,7 +375,7 @@ export default function SchuldTab() { display: "flex", flexDirection: "column", alignItems: "center", }}> ({ val: b.subtot, color: b.color }))} + segments={schuldCats.map((b) => ({ val: b.subtot, color: b.color, naam: b.naam }))} total={donutTotal} size={200} label="Totaal schuld" centerVal={fmt(donutTotal)} />
diff --git a/frontend/src/pages/VoortgangTab.jsx b/frontend/src/pages/VoortgangTab.jsx index fc62f00..06c2004 100644 --- a/frontend/src/pages/VoortgangTab.jsx +++ b/frontend/src/pages/VoortgangTab.jsx @@ -347,7 +347,7 @@ export default function VoortgangTab() { {/* Donut */} - +
diff --git a/release.ps1 b/release.ps1 index 0f27070..4dc0fad 100644 --- a/release.ps1 +++ b/release.ps1 @@ -14,6 +14,8 @@ # - docker buildx builder actief: docker buildx create --use # - Git geïnstalleerd en working directory is de repo root # - root package.json bevat een schoon versienummer ZONDER -dev suffix +# - $env:GITEA_TOKEN gezet (token met release-rechten) — nodig voor stap 4, +# de Gitea-release waar de update-melding in de app op afgaat param( [switch]$NoBuild, @@ -87,6 +89,27 @@ function Write-Step { Write-Host "── $Label" -ForegroundColor Cyan } +# Haalt de sectie van deze versie uit Changelog.md als release-notes. +function Get-ChangelogSection { + param([string]$Version) + $path = Join-Path $PSScriptRoot "Changelog.md" + if (-not (Test-Path $path)) { return "" } + $lines = Get-Content $path + $start = -1 + $needle = "^##\s*\[" + [regex]::Escape($Version) + "\]" + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match $needle) { $start = $i + 1; break } + } + if ($start -lt 0) { return "" } + $out = @() + for ($i = $start; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match "^##\s*\[") { break } + if ($lines[$i] -match "^---\s*$") { continue } + $out += $lines[$i] + } + return (($out -join "`n").Trim()) +} + # ── Samenvatting ─────────────────────────────────────────────────────────── Write-Host "" Write-Host "════════════════════════════════════════" -ForegroundColor Cyan @@ -100,7 +123,7 @@ if ($Force) { Write-Host " Mode : FORCE (conflictcontrole overgeslagen)" - Write-Host "════════════════════════════════════════" -ForegroundColor Cyan # ── Stap 1: Conflictcontrole ────────────────────────────────────────────── -Write-Step "[1/3] Conflictcontrole" +Write-Step "[1/4] Conflictcontrole" if (-not $DryRun -and -not $Force) { $localTag = git tag --list $gitTag @@ -144,7 +167,7 @@ if (-not $DryRun -and -not $Force) { # ── Stap 2: Bouwen en pushen ────────────────────────────────────────────── if (-not $NoBuild) { - Write-Step "[2/3] Multi-platform bouwen en pushen" + Write-Step "[2/4] Multi-platform bouwen en pushen" Write-Host "Backend bouwen en pushen..." -ForegroundColor White Invoke-Cmd @( @@ -169,11 +192,11 @@ if (-not $NoBuild) { (Join-Path $PSScriptRoot "frontend") ) } else { - Write-Step "[2/3] Bouwen overgeslagen (-NoBuild)" + Write-Step "[2/4] Bouwen overgeslagen (-NoBuild)" } # ── Stap 3: Git-tag aanmaken en pushen ──────────────────────────────────── -Write-Step "[3/3] Git-tag aanmaken en pushen" +Write-Step "[3/4] Git-tag aanmaken en pushen" if ($Force) { Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow @@ -192,6 +215,57 @@ Invoke-Cmd @("git", "tag", "-a", $gitTag, "-m", "Release $gitTag") Write-Host "Git-tag pushen naar origin" -ForegroundColor White Invoke-Cmd @("git", "push", "origin", "refs/tags/$gitTag") +# ── Stap 4: Gitea-release aanmaken ──────────────────────────────────────── +Write-Step "[4/4] Gitea-release aanmaken" + +# De update-melding in de app leest /releases/latest. Een kale Git-tag komt +# daar niet in terug, dus er moet een echte release van gemaakt worden. +$repoUrl = ($rootPkg.repository -replace "/+$", "") +$releaseApi = $null +if ($repoUrl -match "^(https?://[^/]+)/([^/]+)/([^/]+)$") { + $releaseApi = "$($Matches[1])/api/v1/repos/$($Matches[2])/$($Matches[3])/releases" +} +$giteaToken = $env:GITEA_TOKEN + +if (-not $releaseApi) { + Write-Host "Kon 'repository' niet parsen uit package.json — release overgeslagen." -ForegroundColor Yellow +} elseif (-not $giteaToken) { + 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 ' Token met release-rechten zetten: $env:GITEA_TOKEN = ""' -ForegroundColor DarkGray +} else { + $notes = Get-ChangelogSection $appVersion + if (-not $notes) { $notes = "Release $appVersion" } + $payload = @{ + tag_name = $gitTag + name = "v$appVersion" + body = $notes + draft = $false + prerelease = $false + } | ConvertTo-Json -Depth 3 + + if ($DryRun) { + Write-Host "[DRY-RUN] POST $releaseApi" -ForegroundColor Yellow + Write-Host $payload -ForegroundColor DarkGray + } else { + Write-Host "Release publiceren: $releaseApi" -ForegroundColor White + try { + $rel = Invoke-RestMethod -Method Post -Uri $releaseApi -Body $payload ` + -ContentType "application/json" ` + -Headers @{ Authorization = "token $giteaToken" } + Write-Host "Release aangemaakt: $($rel.html_url)" -ForegroundColor Green + } catch { + $code = $_.Exception.Response.StatusCode.value__ + if ($code -eq 409) { + Write-Host "Release voor $gitTag bestaat al — overgeslagen." -ForegroundColor Yellow + } else { + Write-Host "Release aanmaken mislukt ($code): $($_.Exception.Message)" -ForegroundColor Red + Write-Host "Tag en images staan al klaar; maak de release eventueel handmatig aan." -ForegroundColor DarkGray + } + } + } +} + # ── Klaar ───────────────────────────────────────────────────────────────── Write-Host "" Write-Host "════════════════════════════════════════" -ForegroundColor Cyan diff --git a/release.sh b/release.sh index 36474d2..61bbea7 100644 --- a/release.sh +++ b/release.sh @@ -19,6 +19,8 @@ # - Ingelogd op de registry: docker login vibetea.vldn.net # - Git geïnstalleerd # - root package.json bevat versienummer ZONDER -dev suffix +# - GITEA_TOKEN in de omgeving (token met release-rechten) — nodig voor het +# aanmaken van de Gitea-release waar de update-melding op afgaat set -euo pipefail