Skip to main content

max / goingson

Give events explicit timezone semantics; expose task recurrence in go-mcp An event was a bare UTC instant and its recurrence rule was expanded against system_tz() at read time, which made the schedule a property of the reading machine rather than of the data. The same database opened in Denver and in Lisbon produced different local times and different weekdays, because a rule anchored at 01:00 UTC is Monday in one and Tuesday in the other. Events now carry a kind, following iCalendar's distinction: relative civil wall clock, no zone. "06:00 wherever I am", so a personal routine follows the user instead of staying pinned to a longitude. local civil wall clock plus an IANA zone. Correct across that zone's DST for every reader. iCalendar's TZID. absolute a fixed instant, displayed in the reader's zone. How every row was implicitly treated until now. For the civil kinds the wall clock is the truth and start_time/end_time hold a materialized UTC projection, kept so every existing range query, index and ORDER BY works untouched. The projection is derived, never authoritative, which is also why the civil columns are what a sync peer trusts: a relative event synced to a machine in another zone must be re-materialized there, not overwritten with this one's instant. A startup pass recomputes it when the system zone changes, gated on a device-local marker so an ordinary launch does one SELECT and no writes. Migration 063 is behaviour-preserving: every existing row becomes absolute. Reclassifying a routine is a deliberate act on a user's own data, not something a migration should guess, so it lives in scripts/classify_routines_relative.sql. Imports stay absolute; the .ics parser resolves TZID and Z to an instant before we see it, and a foreign calendar never describes "whenever I happen to be". Separately, create_task, bulk_import_tasks and update_task now take the same recurrence argument the event tools do. That exposed a gap: complete_task went through the plain repository complete, so a recurring task would have closed with no successor. It now goes through complete_recurring, and the logic that builds the successor moved out of the desktop command layer into goingson_core::next_recurring_task, since the MCP peer is a second writer and two copies would be two answers to what recurring completion means. Two bugs found on the way. The event form posted civil times space-separated, the shape SQLite stores, which chrono's ISO 8601 deserializer rejects outright, so every relative or local event created from the UI would have failed at the serde boundary. And getEventFormFields read event.start_time where EventResponse serializes camelCase, so edit mode had always silently fallen back to the current time instead of the event's own.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-27 17:26 UTC
Signed with PGP, not checked
Commit: dd5c6d77f4c8d7eb575407f64b1700d46e22073a
Parent: 989afea
31 files changed, +1648 insertions, -150 deletions
@@ -63,3 +63,4 @@
63 63 060 e5b833cff6c768710b0daf88fc9cf087938278ec158c99c0789242878832e802ae00e82bef2cc45efe21644de8d9fa57
64 64 061 8a37aa99045c49df8e54d36227702e68f570abb870114da14b31760cd0a1433cdd8f0d234808ec0d0da22672bc7fcb11
65 65 062 6a5d413c09ee5c3a115952dbeb1836e140d8170c24f4bab17a751b2d9e67cf583c1fff3639409230c1b7f56c851abaeb
66 + 063 a1471b8f79dd273af634ceb28b253a4ac7dc4e973b1172a25f348f3f81f927bcf4f5f5aaf30cd89d2d6f216ae1ccfdbf
@@ -48,6 +48,10 @@
48 48 // per machine, so syncing it would hand one device another's endpoint.
49 49 // Its bearer token is a secret and lives in the OS keychain, not here.
50 50 ("wam_url", Posture::Local),
51 + // The zone this device last materialized relative events against. It
52 + // records where *this* machine was, so syncing it would let a peer in
53 + // another zone convince this one that no recompute is due.
54 + ("events_materialized_tz", Posture::Local),
51 55 ],
52 56 );
53 57