Tasks goals: auto metrics (tasks.goals.metric_key/metric_detail/metric_computed_at, tasks.metric_sources cache). €10k/month goal gets a live progress bar: Revolut recurring income (excl. Zorgtoeslag/Joint) computed on page load + trailing-12m paid Moneybird invoices / 12 cached by a daily goal-metrics user timer; stacked bar with %, amount vs target, salary/side-income breakdown, last-updated
Some checks failed
Deploy production / deploy (push) Failing after 1m14s

This commit is contained in:
2026-10-26 01:17:29 +00:00
parent 5c6ce75409
commit 5d732fc750
10 changed files with 40489 additions and 4 deletions

46
scripts/goal-metrics.ts Normal file
View File

@@ -0,0 +1,46 @@
// Daily refresh of auto goal metrics (tasks.goals.metric_key):
// 1. trailing-12-month paid Moneybird sales invoices -> tasks.metric_sources
// 2. recompute every auto goal -> tasks.goals.metric_current/metric_detail (+ progress snapshot)
//
// bun --env-file=$HOME/.config/goal-metrics.env scripts/goal-metrics.ts [--dry-run]
// Env: DB_URL (app role), MONEYBIRD_API_TOKEN, MONEYBIRD_ADMINISTRATION_ID. Mode 600, never committed.
// Run by the goal-metrics.timer user unit (deploy/systemd). Page loads never call Moneybird.
import postgres from 'postgres';
import {
SIDE_INCOME_SOURCE,
fetchMoneybirdSideIncome,
persistGoalMetrics,
saveSource
} from '../src/lib/server/goal-metrics/core';
const dry = process.argv.includes('--dry-run');
const { DB_URL, MONEYBIRD_API_TOKEN, MONEYBIRD_ADMINISTRATION_ID } = process.env;
if (!DB_URL) throw new Error('Set DB_URL');
const sql = postgres(DB_URL, { max: 1, connect_timeout: 10, onnotice: () => {} });
let failed = false;
try {
if (!MONEYBIRD_API_TOKEN || !MONEYBIRD_ADMINISTRATION_ID) {
failed = true;
console.error('[goal-metrics] Moneybird env missing; keeping the cached side income');
} else {
try {
const mb = await fetchMoneybirdSideIncome(MONEYBIRD_API_TOKEN, MONEYBIRD_ADMINISTRATION_ID);
console.log(`[goal-metrics] Moneybird: ${mb.invoices} paid invoices ${mb.from}..${mb.to}`);
if (!dry) await saveSource(sql, SIDE_INCOME_SOURCE, mb.monthly, mb, null);
} catch (e) {
failed = true;
const msg = (e as Error).message.slice(0, 300);
console.error(`[goal-metrics] Moneybird failed: ${msg}`);
if (!dry) await saveSource(sql, SIDE_INCOME_SOURCE, null, null, msg);
}
}
if (!dry) {
for (const g of await persistGoalMetrics(sql))
console.log(
`[goal-metrics] goal ${g.id}: ${g.target ? ((g.value / g.target) * 100).toFixed(1) + '%' : 'no target'}`
);
}
} finally {
await sql.end();
}
if (failed) process.exit(1);