Calendar: convert past events into log entries after the fact ("Log this") #47

Closed
opened 2026-09-24 15:16:26 +00:00 by kayg · 12 comments
Owner

Owner decision C14 (DESIGN §30): "any future events should be logged to a CalDAV server of choice and can be converted to markdown logs AFTER the fact".

  • Past external events get a "Log this" action (popover, context menu, and a subtle affordance on unlogged past events in the day/week view) that creates a log entry in the right daily note: time range from the event (in the event's zone), title from the event summary, and a link back to the event (child bullet or property with provider/calendar/UID, plus the event URL if set).
  • Batch: "Log today's events" on a day header, letting the user tick which ones happened and adjust times.
  • An event already logged shows a small "logged" mark, and the plan-vs-actual view pairs the two.
  • Never modifies the external event unless the user asks.

Context: log entries are authoritative in calternal (daily notes, ## 📝 Log); external events stay at their provider with a derived cache (#40). Related: #39, #42, #44. Read CLAUDE.md, CONTEXT.md, docs/DESIGN.md §29–§30. Comment on this issue as you work; never close it.

Owner decision C14 (DESIGN §30): "any future events should be logged to a CalDAV server of choice and can be converted to markdown logs AFTER the fact". - Past external events get a **"Log this"** action (popover, context menu, and a subtle affordance on unlogged past events in the day/week view) that creates a log entry in the right daily note: time range from the event (in the event's zone), title from the event summary, and a link back to the event (child bullet or property with provider/calendar/UID, plus the event URL if set). - Batch: "Log today's events" on a day header, letting the user tick which ones happened and adjust times. - An event already logged shows a small "logged" mark, and the plan-vs-actual view pairs the two. - Never modifies the external event unless the user asks. Context: log entries are authoritative in calternal (daily notes, `## 📝 Log`); external events stay at their provider with a derived cache (#40). Related: #39, #42, #44. Read CLAUDE.md, CONTEXT.md, docs/DESIGN.md §29–§30. Comment on this issue as you work; never close it.
Author
Owner

Starting issue #47 on branch job/event-links, based on 6a1d2aa7f1771e84cb2fd38c0c019e3df0f0527a. I am tracing the notes journal writer and calendar cache/client first, then I will implement zone-correct, idempotent log creation without writing to the external event.

Starting issue #47 on branch `job/event-links`, based on `6a1d2aa7f1771e84cb2fd38c0c019e3df0f0527a`. I am tracing the notes journal writer and calendar cache/client first, then I will implement zone-correct, idempotent log creation without writing to the external event.
Author
Owner

The Notes Journal writer already serializes mutations with a per-User lock, then performs a conditional file replacement and updates the derived Index. The Calendar endpoint will call a Notes-owned helper for “Log this” so the Calendar plugin does not write the Data directory directly.

The Notes Journal writer already serializes mutations with a per-User lock, then performs a conditional file replacement and updates the derived Index. The Calendar endpoint will call a Notes-owned helper for “Log this” so the Calendar plugin does not write the Data directory directly.
Author
Owner

Focused Notes concurrency test found that Calendar joined account, Calendar, and provider UID with NUL bytes, while the Notes writer rejects control characters (invalid Event identity). The key now uses UTF-8 byte-length prefixes, which keep the tuple unambiguous without control bytes. Added a regression test for the key encoding; the Notes idempotency and Calendar key tests pass.

Focused Notes concurrency test found that Calendar joined account, Calendar, and provider UID with NUL bytes, while the Notes writer rejects control characters (`invalid Event identity`). The key now uses UTF-8 byte-length prefixes, which keep the tuple unambiguous without control bytes. Added a regression test for the key encoding; the Notes idempotency and Calendar key tests pass.
Author
Owner

Implementation decisions for details not specified in the issue: Log this keys idempotence by account, Calendar, and provider UID, with a stable hashed Log block ID; repeated POST returns the same entry with 200, and GET on the same route reports the entry. All-day Events need explicit local start and end times. Events that cross local midnight need same-day Log times. For a repeated daylight-saving local time, an Event is past only after both possible instants have passed. These rules preserve one-day Log entries with their Event zone.

Implementation decisions for details not specified in the issue: `Log this` keys idempotence by account, Calendar, and provider UID, with a stable hashed Log block ID; repeated POST returns the same entry with 200, and GET on the same route reports the entry. All-day Events need explicit local start and end times. Events that cross local midnight need same-day Log times. For a repeated daylight-saving local time, an Event is past only after both possible instants have passed. These rules preserve one-day Log entries with their Event zone.
Author
Owner

Reviewing deep-link construction found a provider-owned Event UID edge case: url's PathSegmentsMut::extend skips . and .., which would make a Log entry's /e/<account>/<calendar>/<uid> link lose its UID segment. Log this now rejects those two path-token UIDs before writing. I added a fake CalDAV Event with UID .. to the adversarial probe and assert a clean 400.

Reviewing deep-link construction found a provider-owned Event UID edge case: `url`'s `PathSegmentsMut::extend` skips `.` and `..`, which would make a Log entry's `/e/<account>/<calendar>/<uid>` link lose its UID segment. `Log this` now rejects those two path-token UIDs before writing. I added a fake CalDAV Event with UID `..` to the adversarial probe and assert a clean 400.
Author
Owner

Finding from the Calendar event-list contract: expanded recurrence items use API IDs of the form <cached-event-id>@<occurrence-identity>, while Log this initially accepted only the cached event ID. That would reject a Log this action from recurring event rows. The route now resolves the stable occurrence suffix to the provider event and gives each occurrence its own idempotency key. Regression coverage checks accepted/rejected occurrence tokens, correct provider-zone/local-day logging, and distinct IDs per occurrence. Focused test is still compiling; I will report its result with the gate run.

Finding from the Calendar event-list contract: expanded recurrence items use API IDs of the form `<cached-event-id>@<occurrence-identity>`, while Log this initially accepted only the cached event ID. That would reject a Log this action from recurring event rows. The route now resolves the stable occurrence suffix to the provider event and gives each occurrence its own idempotency key. Regression coverage checks accepted/rejected occurrence tokens, correct provider-zone/local-day logging, and distinct IDs per occurrence. Focused test is still compiling; I will report its result with the gate run.
Author
Owner

Focused Calendar verification exposed an ID-format assumption in the new unit tests: expand_event retains the provider-zone offset in timed occurrence IDs. The tests now use the emitted 2026-09-10T00:30:00+14:00 token, and verify that offset-bearing IDs parse and resolve to the right local day. Focused results: test result: ok. 34 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s.

Focused Calendar verification exposed an ID-format assumption in the new unit tests: `expand_event` retains the provider-zone offset in timed occurrence IDs. The tests now use the emitted `2026-09-10T00:30:00+14:00` token, and verify that offset-bearing IDs parse and resolve to the right local day. Focused results: `test result: ok. 34 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s`.
Author
Owner

Workspace clippy exposed that Notifications constructs Calendar's public EventMetadata type directly. To avoid a source-breaking public field addition, Calendar now extracts the optional provider URL through an internal client helper, and Log this uses that helper. No Notifications source change was needed. Focused results: Calendar 34 passed; 0 failed; Notifications 11 passed; 0 failed. Commit: 5a97d1f.

Workspace clippy exposed that Notifications constructs Calendar's public `EventMetadata` type directly. To avoid a source-breaking public field addition, Calendar now extracts the optional provider URL through an internal client helper, and Log this uses that helper. No Notifications source change was needed. Focused results: Calendar `34 passed; 0 failed`; Notifications `11 passed; 0 failed`. Commit: `5a97d1f`.
Author
Owner

The workspace test exposed a stale Notes assertion in daily_and_composer_preserve_unrelated_bytes: the journal-day route already calls the base ensure_ids migration before returning entries, so a legacy line has a stable non-null ID. I updated the assertion to verify both IDs are non-empty, distinct, and the migrated legacy ID is written into Markdown. The isolated test passes; runtime behavior is unchanged. Commit: 3519aaf.

The workspace test exposed a stale Notes assertion in `daily_and_composer_preserve_unrelated_bytes`: the journal-day route already calls the base `ensure_ids` migration before returning entries, so a legacy line has a stable non-null ID. I updated the assertion to verify both IDs are non-empty, distinct, and the migrated legacy ID is written into Markdown. The isolated test passes; runtime behavior is unchanged. Commit: `3519aaf`.
Author
Owner

The required workspace cargo test gate reaches one existing Server test failure: wire::tests::full_app_setup_session_config_and_backup explicitly signs out its cookie at /api/v1/auth/session, then reuses that revoked cookie for DELETE /api/v1/admin/users/{member_id} while expecting 204. The request returns 401. The same assertion fails when the test runs alone, and the test is unchanged from base. I left Server behavior/tests untouched because this job only owns a Server change for the /n/<id> redirect, which already works through the existing SPA fallback. All Notes, Calendar, and Notifications tests pass in the workspace run.

The required workspace `cargo test` gate reaches one existing Server test failure: `wire::tests::full_app_setup_session_config_and_backup` explicitly signs out its `cookie` at `/api/v1/auth/session`, then reuses that revoked cookie for `DELETE /api/v1/admin/users/{member_id}` while expecting 204. The request returns 401. The same assertion fails when the test runs alone, and the test is unchanged from base. I left Server behavior/tests untouched because this job only owns a Server change for the `/n/<id>` redirect, which already works through the existing SPA fallback. All Notes, Calendar, and Notifications tests pass in the workspace run.
Author
Owner

Completed on branch job/event-links at head 78ff3f1e3db42baa697a56783c4ddd96a2471408.

Issue #47 is implemented in Notes as an idempotent Journal write keyed by Event identity and occurrence, with the Event's local date, time, and IANA zone. The stable /e/<account>/<calendar>/<uid> link and provider Event URL are stored as Log child links and exposed in both Notes and Calendar projections. The live production probe passed both past Kiritimati occurrences, distinct Log IDs, retry/concurrency idempotence, status lookup, and the provider UID links.

Gates:

  • cargo fmt --all --check: exit 0; no output.
  • cargo clippy --workspace --all-targets -- -D warnings: exit 0. Final output:
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 23.89s
  • Notes tests: test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out.
  • Calendar tests: test result: ok. 35 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; cache and protocol integration tests also passed.
  • bash packages/api-client/check-generated.sh: exit 0; 🚀 ../../contracts/openapi.json → src/generated.ts [1.6s].
  • cargo test --workspace: exit 101 in the unchanged Server setup test. Exact failure:
---- wire::tests::full_app_setup_session_config_and_backup stdout ----

thread 'wire::tests::full_app_setup_session_config_and_backup' (3479050) panicked at crates/calternal-server/src/wire.rs:1752:51:
called `Result::unwrap()` on an `Err` value: InvalidEnvironmentValue
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    wire::tests::full_app_setup_session_config_and_backup

error: test failed, to rerun pass `-p calternal-server --bin calternal-server`
  • bash tests/adversarial/run.sh: exit 1. There were no Calendar or Log-this findings in round 1. The shared task storm produced 17 latency findings (201 responses, 5.4–12.1s); round 2 produced 13 user Home archive/purge/transfer/deletion findings in existing non-owned routes. The complete finding blocks are in the final #44 comment.

No changes were made to user Home archive, purge, transfer, or deletion routes because they are outside this job's owned files. Build output was cleaned: cargo clean removed 11.3 GiB from target-calendar-test and 6.8 GiB from target.

Completed on branch `job/event-links` at head `78ff3f1e3db42baa697a56783c4ddd96a2471408`. Issue #47 is implemented in Notes as an idempotent Journal write keyed by Event identity and occurrence, with the Event's local date, time, and IANA zone. The stable `/e/<account>/<calendar>/<uid>` link and provider Event URL are stored as Log child links and exposed in both Notes and Calendar projections. The live production probe passed both past Kiritimati occurrences, distinct Log IDs, retry/concurrency idempotence, status lookup, and the provider UID links. Gates: - `cargo fmt --all --check`: exit 0; no output. - `cargo clippy --workspace --all-targets -- -D warnings`: exit 0. Final output: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 23.89s ``` - Notes tests: `test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out`. - Calendar tests: `test result: ok. 35 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out`; cache and protocol integration tests also passed. - `bash packages/api-client/check-generated.sh`: exit 0; `🚀 ../../contracts/openapi.json → src/generated.ts [1.6s]`. - `cargo test --workspace`: exit 101 in the unchanged Server setup test. Exact failure: ``` ---- wire::tests::full_app_setup_session_config_and_backup stdout ---- thread 'wire::tests::full_app_setup_session_config_and_backup' (3479050) panicked at crates/calternal-server/src/wire.rs:1752:51: called `Result::unwrap()` on an `Err` value: InvalidEnvironmentValue note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: wire::tests::full_app_setup_session_config_and_backup error: test failed, to rerun pass `-p calternal-server --bin calternal-server` ``` - `bash tests/adversarial/run.sh`: exit 1. There were no Calendar or Log-this findings in round 1. The shared task storm produced 17 latency findings (201 responses, 5.4–12.1s); round 2 produced 13 user Home archive/purge/transfer/deletion findings in existing non-owned routes. The complete finding blocks are in the final #44 comment. No changes were made to user Home archive, purge, transfer, or deletion routes because they are outside this job's owned files. Build output was cleaned: `cargo clean` removed 11.3 GiB from `target-calendar-test` and 6.8 GiB from `target`.
Author
Owner

Completed on dev in b05051e41e (Merge job/event-links: note/log ↔ event links and "Log this" (#44, #47)).

Completed on dev in b05051e41e1163d0f4aad094ba7ac2dd28f21552 (Merge job/event-links: note/log ↔ event links and "Log this" (#44, #47)).
kayg closed this issue 2026-10-01 05:08:49 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#47
No description provided.