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

View File

@@ -6,6 +6,7 @@ import {
date,
index,
integer,
jsonb,
numeric,
smallint,
text,
@@ -14,6 +15,7 @@ import {
} from 'drizzle-orm/pg-core';
import { tasksSchema } from './schemas';
import { id, timestamps, timestamptz } from './helpers';
import type { GoalMetricDetail } from '../../goal-metrics/core';
// Private task + goal tracker (owner only, see requireOwner); meant to replace TickTick.
// No user_id: there is exactly one owner, and the schema is granted to personal_app only.
@@ -60,6 +62,10 @@ export const todoGoals = tasksSchema.table(
metricUnit: text('metric_unit'), // e.g. "EUR"
metricTarget: numeric('metric_target', { precision: 14, scale: 2, mode: 'number' }),
metricCurrent: numeric('metric_current', { precision: 14, scale: 2, mode: 'number' }),
// Auto metric (src/lib/server/goal-metrics): when set, metric_current is computed, not typed.
metricKey: text('metric_key'),
metricDetail: jsonb('metric_detail').$type<GoalMetricDetail>(), // breakdown of the last computation
metricComputedAt: timestamptz('metric_computed_at'),
flagged: boolean('flagged').notNull().default(false),
sortOrder: integer('sort_order').notNull().default(0),
source: text('source'), // e.g. 'daisy-import', 'manual', 'ticktick'
@@ -73,6 +79,17 @@ export const todoGoals = tasksSchema.table(
]
);
/** Cached inputs for auto goal metrics that come from slow/external APIs (e.g. Moneybird),
* refreshed daily by scripts/goal-metrics.ts so page loads never call them. */
export const todoMetricSources = tasksSchema.table('metric_sources', {
key: text('key').primaryKey(),
value: numeric('value', { precision: 14, scale: 2, mode: 'number' }),
detail: jsonb('detail'),
computedAt: timestamptz('computed_at').notNull().defaultNow(), // last successful refresh
attemptedAt: timestamptz('attempted_at').notNull().defaultNow(),
error: text('error') // last refresh error (value stays the last good one)
});
export const todoTasks = tasksSchema.table(
'tasks',
{