Donut-hover met percentages en werkende update-notificatie
- Donut: naadloze segmenten (donkere spleten weg), hover-tooltip per categorie met naam, bedrag en aandeel, middenwaarde schaalt mee - Update-check via de eigen backend (/api/update-check) i.p.v. direct naar de Gitea API, die geen CORS-headers stuurt - Semver-vergelijking, herhaalde check en release-notes in de melding - release.ps1 maakt de Gitea-release aan (stap 4) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e45d5b73fb
commit
3c67381de9
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user