Originally created by: athanclark
Stage 2 of [#537]: the server stores and queries ongoing durations. This is stacked on [#539] (stage 1) and will be retargeted to main once that merges.
The interval trees (oc_duration_nodes, and oc_view_duration_nodes for saved views) are persisted as exact rationals, so "now" can't be stored in them. Each ongoing duration is stored as a point at its start, with its definition keeping "end": {"ongoing": true}. That needs no tree schema change, and it keeps forks, history and lookups working.
Every query that reads durations takes those placeholders out and adds the timeline's ongoing durations, resolved at the server's clock (max(start, now)):
| Query | What changes |
|---|---|
overview, bands |
Merged by withOngoing, so the longest-first ranking, the 256 kept and durationsTotal match the browser. The walk never meets a placeholder: the threshold is always above zero and a placeholder has no length. |
overview, summaries |
Each placeholder is removed from the cluster holding its start, then the resolved band is added by summariesWithOngoing if it collapses. |
durations with overlapping |
Ongoing matches (window, threshold, title text) are merged into the page in each sort order. Stored rows are read from the start of the order up to the page's end, so the merged page is exact. |
durations inside a summary |
Ongoing durations wholly inside are merged after the keyset cursor. |
duration by ID |
The band ends now. |
A timeline's ongoing durations are found through new partial indexes (… WHERE band->'end'->>'ongoing'='true'), at most 10,000 per query. A timeline without any pays one empty index scan. If a timeline's display stops being Gregorian, its ongoing durations stay points at their starts.
The OpenAPI schema now describes the ongoing end, and the stage 1 refusal on the server is removed.
test/ongoing-durations-postgres.mjs (added to CI): full and sparse saves, closing and reopening, windows before, around and after now, collapsing when zoomed out (counted once, never twice), list pages in every order, the title filter, durations inside a summary, lookup by ID, and refusal on non-Gregorian timelines.durations-postgres, summaries-postgres (150 random views match the browser index), views-postgres and openapi-postgres.npm test passes apart from contents.test.mjs. It fails locally with git ENOBUFS, caused by untracked worktrees.Stage 3: the SQLite native store and och-convert.
🤖 Generated with Claude Code
Ticket changed by: athanclark