Files
verdande/internal/store/migrations/0003_feeds_templates.sql
T
Kristian WindandClaude Opus 5 7e4c730049 feat(phase3): reminders, ICS feeds, templates and nightly backups
Reminders are absolute or relative ("10 minutes before"). A relative one
is resolved against the task in SQL, so moving a task moves its reminders
too. They are marked sent before delivery, not after: a reminder that
goes out unrecorded repeats every minute forever, which is worse than
one recorded that fails to send.

The calendar feed is a per-user token in the path, because Apple Calendar
and Google fetch with no credentials and no way to prompt for any. Tasks
go out as VTODO — the same component CalDAV will serve — and timed ones
also as VEVENT, since Google ignores VTODO entirely.

Templates store relative day offsets rather than dates, which is what
makes them reusable: an onboarding checklist is "day 1, day 3, day 14",
not three dates in March. Building a project from one is a single
transaction, so a failure cannot leave half a checklist.

Backups use SQLite's VACUUM INTO, not a file copy — a copy of a live
WAL database can miss recent writes or catch a page mid-update, and the
result looks like a database right up until it is needed. Rotation keeps
fourteen by count, not by age: a container that was off for a month must
not come back and delete every backup it has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 07:05:24 +02:00

52 lines
2.3 KiB
SQL

-- Calendar feeds, project templates and reminder delivery.
-- A per-user token for the read-only calendar feed.
--
-- Calendar clients cannot log in — Apple Calendar and Google both fetch a URL with
-- no credentials and no way to prompt for any — so the URL itself is the
-- capability. That is why the token is separate from the session and from the API
-- token: it can be rotated on its own when a feed URL ends up somewhere it should
-- not, without signing the person out of everything else.
ALTER TABLE users ADD COLUMN ics_token TEXT;
CREATE UNIQUE INDEX idx_users_ics_token ON users (ics_token) WHERE ics_token IS NOT NULL;
-- A project saved as a shape to reuse: its sections and tasks, without dates,
-- assignees or anything else that belonged to the run it was captured from.
--
-- The body is JSON rather than a set of tables. A template is written once and read
-- whole; normalising it would buy queries nobody makes, and cost a migration every
-- time a task gains a field.
CREATE TABLE project_templates (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT 'graphite',
body_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX idx_templates_user ON project_templates (user_id);
-- Reminders already exist as a table; what was missing is the ability to find the
-- ones that are due. A relative reminder ("10 minutes before") has no absolute time
-- of its own, so the scheduler resolves it against the task's due_datetime — and
-- this index is what keeps that sweep from being a full scan every minute.
CREATE INDEX idx_reminders_task ON reminders (task_id) WHERE sent_at IS NULL;
-- Where a nightly backup got to. One row per run, so the panel can show when the
-- last one succeeded and an operator can tell "no backups configured" apart from
-- "backups have been failing for a week".
CREATE TABLE backup_runs (
id TEXT PRIMARY KEY,
started_at INTEGER NOT NULL,
finished_at INTEGER,
path TEXT,
size_bytes INTEGER,
error TEXT
);
CREATE INDEX idx_backup_runs_started ON backup_runs (started_at DESC);