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>
31 files changed,
+1648 insertions,
-150 deletions
| 63 |
63 |
|
060 e5b833cff6c768710b0daf88fc9cf087938278ec158c99c0789242878832e802ae00e82bef2cc45efe21644de8d9fa57
|
| 64 |
64 |
|
061 8a37aa99045c49df8e54d36227702e68f570abb870114da14b31760cd0a1433cdd8f0d234808ec0d0da22672bc7fcb11
|
| 65 |
65 |
|
062 6a5d413c09ee5c3a115952dbeb1836e140d8170c24f4bab17a751b2d9e67cf583c1fff3639409230c1b7f56c851abaeb
|
|
66 |
+ |
063 a1471b8f79dd273af634ceb28b253a4ac7dc4e973b1172a25f348f3f81f927bcf4f5f5aaf30cd89d2d6f216ae1ccfdbf
|
| 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 |
|
|