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>
52 lines
2.3 KiB
SQL
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);
|