CalDAV: expose each area/<tag> as its own calendar (grill first) #356

Closed
opened 2026-09-28 15:36:12 +00:00 by kayg · 32 comments
Owner

Request (owner, 2026-09-28)

"for logging / journal, can we not modify the caldav adapter to expose every area/ as its own calendar? right now I only see one calendar in the apple calendar as journal"

Status

Grill first: open decisions put to the owner. Do not build until DESIGN.md records the answers.

Open questions:

  • A1: which tags become calendars: only #area/<name> (recommended), or every tag.
  • A2: a log entry with two area tags: first area tag wins (recommended), or it appears in both calendars (duplicate UIDs are a problem for Apple).
  • A3: untagged entries stay in "Journal" (recommended).
  • A4: creating an event in the "Work" calendar in Apple Calendar writes a log line with #area/work; moving an event between calendars rewrites the tag (recommended).
  • A5: colour: from the area's tag colour in calternal, editable from Apple (stored per user, same store as the PROPPATCH fix).
  • A6: an area that has no entries yet: it still shows as a calendar if the tag exists anywhere (recommended), so a new area can be targeted from Apple.

Depends on the PROPPATCH colour fix (per-user collection properties).

## Request (owner, 2026-09-28) "for logging / journal, can we not modify the caldav adapter to expose every area/<tag> as its own calendar? right now I only see one calendar in the apple calendar as journal" ## Status Grill first: open decisions put to the owner. Do not build until DESIGN.md records the answers. Open questions: - A1: which tags become calendars: only `#area/<name>` (recommended), or every tag. - A2: a log entry with two area tags: first area tag wins (recommended), or it appears in both calendars (duplicate UIDs are a problem for Apple). - A3: untagged entries stay in "Journal" (recommended). - A4: creating an event in the "Work" calendar in Apple Calendar writes a log line with `#area/work`; moving an event between calendars rewrites the tag (recommended). - A5: colour: from the area's tag colour in calternal, editable from Apple (stored per user, same store as the PROPPATCH fix). - A6: an area that has no entries yet: it still shows as a calendar if the tag exists anywhere (recommended), so a new area can be targeted from Apple. Depends on the PROPPATCH colour fix (per-user collection properties).
Author
Owner

Decided (owner, 2026-09-28): see docs/DESIGN.md §46 (commit fcba3cb1)

Owner answers: A1 only area tags, shown as the Sentence-case leaf (area/aayushy → "Aayushy"). A2 an entry with two areas appears in both calendars; editing one copy must not break the other; copying the same event into another calternal calendar must add the extra tag, not duplicate the line. A3 untagged → "Untagged" calendar. A4 yes (create writes the tag, move rewrites it). A5 same colour in both places, synced through the tag colour. A6 empty areas still show.

Build

  • Replace the single journal collection with one collection per area plus untagged. Keep old journal/ hrefs answering 404/410 or redirecting so existing Apple accounts re-discover cleanly; prove Apple drops the old calendar without an error.
  • Per-copy UID/href = f(block id, area identity). ETag from the rendered log line and area. Calendar ctag/sync-token changes when any entry in it changes, including through the sibling copy.
  • DELETE on a copy removes that area tag; last copy deletes the entry. MOVE / Apple's delete+PUT move replaces the tag.
  • Duplicate detection on PUT: same day, start, end, title → add the area tag to the existing line.
  • Colour: PROPFIND calendar-color = the tag colour (user override from settings tagColors, else the default from packages/ui/src/tags.ts tagColor) as #RRGGBB. PROPPATCH writes the tagColors override. The Rust default must equal the TS default: one source of truth (generate or share a table) plus a cross-language test vector. Untagged colour: stored in the same settings map under a reserved key.
  • Tests: replay fixtures from macOS 27 for create, move, copy (option-drag), delete of one copy, colour change; property test that the two copies of a multi-area entry always render the same line.
  • Adversarial: cursed tag names (Unicode, RTL, .., /, very long), 1000 areas, concurrent edits to both copies, cross-user access to another user's area collection.
  • Depends on #355 (PROPPATCH handling). Start after #355 merges.

Done when

Gates green; Claude proves on the macOS VM: area calendars listed with the right names and colours, colour change round-trips both ways, multi-area edit keeps both copies consistent, copy adds the tag.

## Decided (owner, 2026-09-28): see docs/DESIGN.md §46 (commit fcba3cb1) Owner answers: A1 only area tags, shown as the Sentence-case leaf (`area/aayushy` → "Aayushy"). A2 an entry with two areas appears in both calendars; editing one copy must not break the other; copying the same event into another calternal calendar must add the extra tag, not duplicate the line. A3 untagged → "Untagged" calendar. A4 yes (create writes the tag, move rewrites it). A5 same colour in both places, synced through the tag colour. A6 empty areas still show. ## Build - Replace the single `journal` collection with one collection per area plus `untagged`. Keep old `journal/` hrefs answering 404/410 or redirecting so existing Apple accounts re-discover cleanly; prove Apple drops the old calendar without an error. - Per-copy UID/href = f(block id, area identity). ETag from the rendered log line and area. Calendar ctag/sync-token changes when any entry in it changes, including through the sibling copy. - DELETE on a copy removes that area tag; last copy deletes the entry. MOVE / Apple's delete+PUT move replaces the tag. - Duplicate detection on PUT: same day, start, end, title → add the area tag to the existing line. - Colour: PROPFIND `calendar-color` = the tag colour (user override from settings `tagColors`, else the default from `packages/ui/src/tags.ts` `tagColor`) as #RRGGBB. PROPPATCH writes the `tagColors` override. The Rust default must equal the TS default: one source of truth (generate or share a table) plus a cross-language test vector. Untagged colour: stored in the same settings map under a reserved key. - Tests: replay fixtures from macOS 27 for create, move, copy (option-drag), delete of one copy, colour change; property test that the two copies of a multi-area entry always render the same line. - Adversarial: cursed tag names (Unicode, RTL, `..`, `/`, very long), 1000 areas, concurrent edits to both copies, cross-user access to another user's area collection. - Depends on #355 (PROPPATCH handling). Start after #355 merges. ## Done when Gates green; Claude proves on the macOS VM: area calendars listed with the right names and colours, colour change round-trips both ways, multi-area edit keeps both copies consistent, copy adds the tag.
Author
Owner

Starting #356 on job/area-calendars at base 00015e652a. Read CLAUDE.md, CONTEXT.md, DESIGN §46, and issue. Existing DAV has one journal collection; NotesJournalProvider serializes writes per user and maintains a journal change feed. Implementing area projection over that provider, keeping reminders.rs untouched.

Starting #356 on job/area-calendars at base 00015e652a153485d9e7adda1c5ee61bc17300db. Read CLAUDE.md, CONTEXT.md, DESIGN §46, and issue. Existing DAV has one journal collection; NotesJournalProvider serializes writes per user and maintains a journal change feed. Implementing area projection over that provider, keeping reminders.rs untouched.
Author
Owner

Finding: the shared tags Index contains tags from Notes, Tasks, Files and Photos, keyed by owner_id. It can discover empty area calendars without scanning every Daily note. The existing journal change feed is global per User, so a sibling copy edit advances its token; each projected copy's ETag is derived from the source line plus its area identity. A direct area path is accepted only when its digest resolves to one of that User's indexed tags.

Finding: the shared `tags` Index contains tags from Notes, Tasks, Files and Photos, keyed by owner_id. It can discover empty area calendars without scanning every Daily note. The existing journal change feed is global per User, so a sibling copy edit advances its token; each projected copy's ETag is derived from the source line plus its area identity. A direct area path is accepted only when its digest resolves to one of that User's indexed tags.
Author
Owner

Finding: the web's generated tag foreground is OKLCH (L=0.50, C=0.12, FNV-1a hue over UTF-16 units), while Apple PROPFIND requires opaque sRGB hex. Added a Rust conversion and a shared cross-language vector checked from the web tagColor output; tagColors remains the single override store. No UI rendering code changed.

Finding: the web's generated tag foreground is OKLCH (L=0.50, C=0.12, FNV-1a hue over UTF-16 units), while Apple PROPFIND requires opaque sRGB hex. Added a Rust conversion and a shared cross-language vector checked from the web tagColor output; `tagColors` remains the single override store. No UI rendering code changed.
Author
Owner

Committed first slice fcac7bfd: stable path-safe area and per-copy IDs, creator UID preservation, shared OKLCH-to-sRGB default color vectors, and tagColors integration. cargo test -p calternal-dav passed (27 unit, 14 replay; no failures) and targeted web color test passed (1 test). Current uncommitted slice adds area collection discovery and conditional reads/writes; replay covers duplicate copy, sibling edit, stale If-Match and one-copy deletion. Server crate compilation is in progress.

Committed first slice fcac7bfd: stable path-safe area and per-copy IDs, creator UID preservation, shared OKLCH-to-sRGB default color vectors, and `tagColors` integration. `cargo test -p calternal-dav` passed (27 unit, 14 replay; no failures) and targeted web color test passed (1 test). Current uncommitted slice adds area collection discovery and conditional reads/writes; replay covers duplicate copy, sibling edit, stale If-Match and one-copy deletion. Server crate compilation is in progress.
Author
Owner

Committed c66943f0: area calendar discovery from the shared tag Index, area-specific sync tokens, per-copy UID/href and ETag, conditional PUT/DELETE, duplicate-match copy, and 410 on old journal URLs. cargo test -p calternal-dav passed after this commit (27 unit, 14 replay, 0 failures). Replay now exercises initial sync, create, duplicate copy, edit through one copy, stale If-Match, delete one copy, last-copy delete, and legacy journal retirement. Notes and server crate gates are compiling next.

Committed c66943f0: area calendar discovery from the shared tag Index, area-specific sync tokens, per-copy UID/href and ETag, conditional PUT/DELETE, duplicate-match copy, and 410 on old journal URLs. `cargo test -p calternal-dav` passed after this commit (27 unit, 14 replay, 0 failures). Replay now exercises initial sync, create, duplicate copy, edit through one copy, stale If-Match, delete one copy, last-copy delete, and legacy journal retirement. Notes and server crate gates are compiling next.
Author
Owner

Finding: the first area read path called the full Journal list for every event resource, which would multiply Daily note scans during Apple sync. The Notes provider now resolves a copy by hashing block IDs from journal_resources and opens only the matching Daily note; creation still scans once for exact duplicate detection. Calendar-home PROPFIND now reads all area colours and order values from one User settings snapshot, not one file read per area. This targets the 1000-area adversarial case.

Finding: the first area read path called the full Journal list for every event resource, which would multiply Daily note scans during Apple sync. The Notes provider now resolves a copy by hashing block IDs from `journal_resources` and opens only the matching Daily note; creation still scans once for exact duplicate detection. Calendar-home PROPFIND now reads all area colours and order values from one User settings snapshot, not one file read per area. This targets the 1000-area adversarial case.
Author
Owner

Finding: an unknown area resource name could trigger a full Daily note scan while holding the Notes User lock. The in-progress slice now resolves only indexed block IDs and returns 404 for an unknown name. A restore must rebuild the resource Index before those restored entries are addressable by direct DAV GET. Also found the legacy adversarial script still targeted journal/; updated it to untagged/ and the area sync-token grammar.

Decision: MOVE requires the destination filename to equal the stable per-area copy ID. This prevents a client from creating a second href for one Log entry. Replay now checks rejection of an alternate filename.

Finding: an unknown area resource name could trigger a full Daily note scan while holding the Notes User lock. The in-progress slice now resolves only indexed block IDs and returns 404 for an unknown name. A restore must rebuild the resource Index before those restored entries are addressable by direct DAV GET. Also found the legacy adversarial script still targeted `journal/`; updated it to `untagged/` and the area sync-token grammar. Decision: MOVE requires the destination filename to equal the stable per-area copy ID. This prevents a client from creating a second href for one Log entry. Replay now checks rejection of an alternate filename.
Author
Owner

Committed a1ab6851: updated DAV documentation and both adversarial probes for untagged/, area sync tokens, retired journal/, hostile area paths, and a 1000-area listing. Python and Node syntax checks passed. The provider and protocol test build remains in progress; no source tests were bypassed.

Committed a1ab6851: updated DAV documentation and both adversarial probes for `untagged/`, area sync tokens, retired `journal/`, hostile area paths, and a 1000-area listing. Python and Node syntax checks passed. The provider and protocol test build remains in progress; no source tests were bypassed.
Author
Owner

Test finding: cargo test -p calternal-plugin-notes --lib ran 110 tests; 109 passed and tasks_dav::performance_tests::reminders_initial_report_timing_at_10_200_and_2000_tasks failed because it still sent REPORT to retired journal/ and received the intended 410 instead of 207. The issue replaces that collection, so I changed only this obsolete request to untagged/ and its expected per-copy ID. No behavior assertion for a still-valid endpoint was relaxed. Re-running the touched crate gate now.

Test finding: `cargo test -p calternal-plugin-notes --lib` ran 110 tests; 109 passed and `tasks_dav::performance_tests::reminders_initial_report_timing_at_10_200_and_2000_tasks` failed because it still sent REPORT to retired `journal/` and received the intended 410 instead of 207. The issue replaces that collection, so I changed only this obsolete request to `untagged/` and its expected per-copy ID. No behavior assertion for a still-valid endpoint was relaxed. Re-running the touched crate gate now.
Author
Owner

Committed 651ebe0e: the DAV provider now resolves one area copy, home discovery reads collection properties in one batch, MOVE requires the stable destination identity, and area sync tokens reject use in another collection. The new property test verifies sibling projections preserve one canonical Log line. cargo test -p calternal-dav passed: 29 unit, 15 replay, 0 failures. The Notes integration rerun is in progress.

Committed 651ebe0e: the DAV provider now resolves one area copy, home discovery reads collection properties in one batch, MOVE requires the stable destination identity, and area sync tokens reject use in another collection. The new property test verifies sibling projections preserve one canonical Log line. `cargo test -p calternal-dav` passed: 29 unit, 15 replay, 0 failures. The Notes integration rerun is in progress.
Author
Owner

Committed fdc43560: direct area resource reads hash indexed block IDs, return 404 on unknown names without a Home scan, and hold the Notes User lock only for ID repair. The one obsolete performance probe now targets untagged/ because journal/ intentionally returns 410. cargo test -p calternal-plugin-notes --lib passed: 110 passed, 0 failed (80.01s). Server colour batching test is in progress.

Committed fdc43560: direct area resource reads hash indexed block IDs, return 404 on unknown names without a Home scan, and hold the Notes User lock only for ID repair. The one obsolete performance probe now targets `untagged/` because `journal/` intentionally returns 410. `cargo test -p calternal-plugin-notes --lib` passed: 110 passed, 0 failed (80.01s). Server colour batching test is in progress.
Author
Owner

Finding: the area resource handler took its per-User area write lock before reading a PUT body. A client that sends the headers and stalls could block all area edits for that User. The in-progress fix buffers the existing 64 KiB bounded body before the lock. A replay regression holds a PUT body open and checks that a conditional DELETE completes while it is pending.

Finding: the area resource handler took its per-User area write lock before reading a PUT body. A client that sends the headers and stalls could block all area edits for that User. The in-progress fix buffers the existing 64 KiB bounded body before the lock. A replay regression holds a PUT body open and checks that a conditional DELETE completes while it is pending.
Author
Owner

Finding: tagColor() in the web lowercases tag names before reading tagColors, but DAV wrote mixed-case area names as exact keys. A PROPPATCH of area/Work could therefore succeed in Apple without changing the web colour. The server adapter now uses the web's lowercased key for DAV reads and writes, with a mixed-case round-trip test. Distinct area spellings retain their separate calendar IDs; the shared tag colour map follows the existing web key rule.

Finding: `tagColor()` in the web lowercases tag names before reading `tagColors`, but DAV wrote mixed-case area names as exact keys. A PROPPATCH of `area/Work` could therefore succeed in Apple without changing the web colour. The server adapter now uses the web's lowercased key for DAV reads and writes, with a mixed-case round-trip test. Distinct area spellings retain their separate calendar IDs; the shared tag colour map follows the existing web key rule.
Author
Owner

Finding: a DELETE followed by PUT move of an entry with one area tag deleted its Log line before duplicate detection could find it. The target PUT then created a different block ID, breaking stable links to that Log entry. The in-progress fix keeps up to 128 recently deleted sources for 120 seconds in process memory, keyed by User and exact event fields. A matching follow-up PUT restores the old block ID with the target area tag; a standalone DELETE still removes the line immediately. Replay now checks the delete-first sequence and returned target href. A server restart between the two requests cannot use this short-lived hint, so that case remains a known limitation pending a durable transfer protocol.

Finding: a DELETE followed by PUT move of an entry with one area tag deleted its Log line before duplicate detection could find it. The target PUT then created a different block ID, breaking stable links to that Log entry. The in-progress fix keeps up to 128 recently deleted sources for 120 seconds in process memory, keyed by User and exact event fields. A matching follow-up PUT restores the old block ID with the target area tag; a standalone DELETE still removes the line immediately. Replay now checks the delete-first sequence and returned target href. A server restart between the two requests cannot use this short-lived hint, so that case remains a known limitation pending a durable transfer protocol.
Author
Owner

Committed b66240b7: bounded PUT bodies are read before the per-User area lock, and a short-lived deleted-source hint preserves the block ID for Apple's delete-first PUT move. Replay checks the move, child-line preservation, stable target href, and that a stalled PUT does not block DELETE. cargo test -p calternal-dav passed: 29 unit tests, 16 replay tests, 0 failures. The hint is process-local and expires after 120 seconds; a server restart between DELETE and PUT can still create a new block ID.

Committed b66240b7: bounded PUT bodies are read before the per-User area lock, and a short-lived deleted-source hint preserves the block ID for Apple's delete-first PUT move. Replay checks the move, child-line preservation, stable target href, and that a stalled PUT does not block DELETE. `cargo test -p calternal-dav` passed: 29 unit tests, 16 replay tests, 0 failures. The hint is process-local and expires after 120 seconds; a server restart between DELETE and PUT can still create a new block ID.
Author
Owner

I revised the delete-first move fix because a process-local hint could lose child lines if the server restarted between Apple requests. Notes now writes a short-lived transfer intent into its existing SQLite migration set before deleting the Log line. On a matching PUT, the provider can reconstruct the original block ID and child lines after a provider restart. The intent is scoped by User, expires after 120 seconds, and is capped at 128 recent rows per User. A new Notes test drops and reopens the provider before reading the hint. The in-memory hint remains the fast path for the same process.

I revised the delete-first move fix because a process-local hint could lose child lines if the server restarted between Apple requests. Notes now writes a short-lived transfer intent into its existing SQLite migration set before deleting the Log line. On a matching PUT, the provider can reconstruct the original block ID and child lines after a provider restart. The intent is scoped by User, expires after 120 seconds, and is capped at 128 recent rows per User. A new Notes test drops and reopens the provider before reading the hint. The in-memory hint remains the fast path for the same process.
Author
Owner

Committed 0d3017ac: Notes persists a bounded 120-second area-delete transfer intent before removing the Log line, so a matching Apple PUT after a server restart can reuse the block ID and child lines. The new provider-reopen regression passed. cargo test -p calternal-plugin-notes --lib passed: 111 passed, 0 failed (55.74s). This follows the existing Notes intent-table pattern; the Daily note remains authoritative after restoration.

Committed 0d3017ac: Notes persists a bounded 120-second area-delete transfer intent before removing the Log line, so a matching Apple PUT after a server restart can reuse the block ID and child lines. The new provider-reopen regression passed. `cargo test -p calternal-plugin-notes --lib` passed: 111 passed, 0 failed (55.74s). This follows the existing Notes intent-table pattern; the Daily note remains authoritative after restoration.
Author
Owner

Committed 7347dd25: the real-server DAV adversarial round now creates two area tags on one Log entry, races conditional PUT edits through both projected copies, expects one 204 and one 412, and checks that both GETs show the same winning summary. Python syntax check passed. The real-server round will run once after the dev merge.

Committed 7347dd25: the real-server DAV adversarial round now creates two area tags on one Log entry, races conditional PUT edits through both projected copies, expects one 204 and one 412, and checks that both GETs show the same winning summary. Python syntax check passed. The real-server round will run once after the dev merge.
Author
Owner

Finding and fix: persisted Apple delete-first move hints remained in SQLite after a successful restore. A later matching PUT could inspect a consumed hint. Added best-effort cleanup after the restored Log write succeeds, plus a provider-reopen regression that confirms the intent no longer resolves. Commit: 450b9519. Evidence: cargo test -p calternal-plugin-notes --lib passed (111 tests); cargo test -p calternal-dav passed (29 unit, 16 replay tests).

Finding and fix: persisted Apple delete-first move hints remained in SQLite after a successful restore. A later matching PUT could inspect a consumed hint. Added best-effort cleanup after the restored Log write succeeds, plus a provider-reopen regression that confirms the intent no longer resolves. Commit: 450b9519. Evidence: `cargo test -p calternal-plugin-notes --lib` passed (111 tests); `cargo test -p calternal-dav` passed (29 unit, 16 replay tests).
Author
Owner

Commit 5bccf64c normalizes DAV colour keys using the same lower-case tag key as the web and reads all collection properties from one User settings snapshot. Its tests cover batched reads, mixed-case round trips, and the reserved Untagged key. cargo test -p calternal-server passed: 81 passed, 0 failed, 2 ignored. The first attempt needed the generated apps/web/build/; bun run build created it, then the server test passed.

Commit 5bccf64c normalizes DAV colour keys using the same lower-case tag key as the web and reads all collection properties from one User settings snapshot. Its tests cover batched reads, mixed-case round trips, and the reserved Untagged key. `cargo test -p calternal-server` passed: 81 passed, 0 failed, 2 ignored. The first attempt needed the generated `apps/web/build/`; `bun run build` created it, then the server test passed.
Author
Owner

Finished

Issue #356 is implemented on job/area-calendars. Head: ac57a55c. The branch push completed; origin/job/area-calendars is up to date. I merged dev once at 7fd41b4f and did not merge into dev.

The CalDAV adapter now exposes one collection per area tag plus Untagged. Copies use stable area-specific UIDs and hrefs, share the source Log line, and refresh sibling ETags and sync tokens after edits. PUT copy detection, area moves, per-tag colours, bounded discovery, and persisted delete-then-PUT recovery are covered by replay and provider tests. The DAV attack probe was extended for hostile area paths, 1000-area listing, and concurrent edits to sibling copies.

Files

  • crates/calternal-dav/src/areas.rs, protocol.rs, README.md, and tests/apple_replay.rs
  • crates/plugins/notes/src/lib.rs, store.rs, tasks_dav.rs, and migrations/0016_dav_area_deletes.sql
  • crates/calternal-server/src/wire.rs
  • apps/web/src/lib/calendar/area-colors.test.ts, contracts/vectors/area-colors.json, and Cargo.lock
  • tests/adversarial/attack.py and tests/adversarial/hostile_bytes.mjs

Gates

  • cargo fmt --check: exit 0; no output.
  • cargo clippy --all-targets -- -D warnings: exit 0. Final output: Finished dev profile [unoptimized + debuginfo] target(s) in 61m 10s
  • cargo test: exit 0. DAV output:
    • test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s
    • test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
    • Server output: test result: ok. 81 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 13.74s
  • bun run check:
    Text sizes use shared role tokens.
    
    svelte-check found 0 errors and 0 warnings
    
  • bun run test:
     Test Files  124 passed (124)
          Tests  788 passed (788)
       Start at  13:14:53
       Duration  152.03s (transform 54%, environment 17%, import 16%, tests 9%, setup 4%)
    
  • Cleanup: Removed 18347 files, 15.0GiB total; removed generated apps/web/build, apps/web/.svelte-kit, and target/e2e-media-runtime.

Adversarial result and gaps

The one DAV-only real-server attempt was timeout 20m env ADVERSARIAL_DAV_ONLY=1 ./tests/adversarial/run.sh. It exited 124 with output Terminated while compiling the server; it did not reach setup or send DAV requests. The adversarial gate is not verified. The requested macOS 27 Apple-client confirmation also remains for the orchestrator.

Decisions for owner confirmation

  • Require a MOVE destination href to use the stable per-area copy ID. This prevents a second href for one Log entry.
  • Store delete-then-PUT recovery hints in the Notes Index for 120 seconds, capped at 128 per User; clear a consumed hint best-effort after the Log write succeeds so cleanup trouble does not turn a successful write into a failed DAV response.
  • Lowercase DAV tagColors keys to match the web tagColor() lookup. Store Untagged colour at _calternal/calendar/untagged.
## Finished Issue #356 is implemented on `job/area-calendars`. Head: `ac57a55c`. The branch push completed; `origin/job/area-calendars` is up to date. I merged `dev` once at `7fd41b4f` and did not merge into `dev`. The CalDAV adapter now exposes one collection per area tag plus Untagged. Copies use stable area-specific UIDs and hrefs, share the source Log line, and refresh sibling ETags and sync tokens after edits. PUT copy detection, area moves, per-tag colours, bounded discovery, and persisted delete-then-PUT recovery are covered by replay and provider tests. The DAV attack probe was extended for hostile area paths, 1000-area listing, and concurrent edits to sibling copies. ## Files - `crates/calternal-dav/src/areas.rs`, `protocol.rs`, `README.md`, and `tests/apple_replay.rs` - `crates/plugins/notes/src/lib.rs`, `store.rs`, `tasks_dav.rs`, and `migrations/0016_dav_area_deletes.sql` - `crates/calternal-server/src/wire.rs` - `apps/web/src/lib/calendar/area-colors.test.ts`, `contracts/vectors/area-colors.json`, and `Cargo.lock` - `tests/adversarial/attack.py` and `tests/adversarial/hostile_bytes.mjs` ## Gates - `cargo fmt --check`: exit 0; no output. - `cargo clippy --all-targets -- -D warnings`: exit 0. Final output: `Finished dev profile [unoptimized + debuginfo] target(s) in 61m 10s` - `cargo test`: exit 0. DAV output: - `test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s` - `test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s` - Server output: `test result: ok. 81 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 13.74s` - `bun run check`: ``` Text sizes use shared role tokens. svelte-check found 0 errors and 0 warnings ``` - `bun run test`: ``` Test Files 124 passed (124) Tests 788 passed (788) Start at 13:14:53 Duration 152.03s (transform 54%, environment 17%, import 16%, tests 9%, setup 4%) ``` - Cleanup: `Removed 18347 files, 15.0GiB total`; removed generated `apps/web/build`, `apps/web/.svelte-kit`, and `target/e2e-media-runtime`. ## Adversarial result and gaps The one DAV-only real-server attempt was `timeout 20m env ADVERSARIAL_DAV_ONLY=1 ./tests/adversarial/run.sh`. It exited `124` with output `Terminated` while compiling the server; it did not reach setup or send DAV requests. The adversarial gate is not verified. The requested macOS 27 Apple-client confirmation also remains for the orchestrator. ## Decisions for owner confirmation - Require a MOVE destination href to use the stable per-area copy ID. This prevents a second href for one Log entry. - Store delete-then-PUT recovery hints in the Notes Index for 120 seconds, capped at 128 per User; clear a consumed hint best-effort after the Log write succeeds so cleanup trouble does not turn a successful write into a failed DAV response. - Lowercase DAV `tagColors` keys to match the web `tagColor()` lookup. Store Untagged colour at `_calternal/calendar/untagged`.
Author
Owner

Merged into dev (09e238d5, with its migration renumbered to 19 because #393 and the daily-note migrations took 16–18). A replay test was fixed for the retired journal collection (28ac39f9). Deploy on hold until job mac-verify proves the upgrade path on the macOS VM: an Apple Calendar already subscribed to the old single 'journal/' collection, which now answers 410.

Merged into dev (09e238d5, with its migration renumbered to 19 because #393 and the daily-note migrations took 16–18). A replay test was fixed for the retired journal collection (28ac39f9). **Deploy on hold** until job mac-verify proves the upgrade path on the macOS VM: an Apple Calendar already subscribed to the old single 'journal/' collection, which now answers 410.
Author
Owner

Starting real-client verification on job/mac-verify, based on dev 28ac39f917. I am using the existing macOS 27 verification VM and will record only protocol and client evidence; no credentials will be included.

Starting real-client verification on job/mac-verify, based on dev 28ac39f917f3dedd44dec2e863ee2854d64c2d5a. I am using the existing macOS 27 verification VM and will record only protocol and client evidence; no credentials will be included.
Author
Owner

Starting real-client verification on job/mac-verify, based on dev 28ac39f917. The macOS 27 VM is reachable. I am using only protocol and client evidence; no credentials will be included.

Starting real-client verification on job/mac-verify, based on dev 28ac39f917f3dedd44dec2e863ee2854d64c2d5a. The macOS 27 VM is reachable. I am using only protocol and client evidence; no credentials will be included.
Author
Owner

Finding: the macOS 27 VM is reachable; Calendar's accessibility tree currently shows the legacy Journal calendar. The logging proxy is listening on 127.0.0.1:18089, but no server listens on :18088, so the proxy closes requests. I found the existing work2 verification data and will preserve it while running the pre-#356 build.

Finding: the macOS 27 VM is reachable; Calendar's accessibility tree currently shows the legacy Journal calendar. The logging proxy is listening on 127.0.0.1:18089, but no server listens on :18088, so the proxy closes requests. I found the existing work2 verification data and will preserve it while running the pre-#356 build.
Author
Owner

Verification started on job/mac-verify, based on dev at 28ac39f917f3dedd44dec2e863ee2854d64c2d5a. The old-build CalDAV account Localhost currently shows its Journal collection in Apple Calendar. Its discovery requests returned 207 after the client was configured with the principal URL. I am preparing the seeded area data before switching the same data to the area-calendar build. No code or deployment changes have been made.

Verification started on `job/mac-verify`, based on `dev` at `28ac39f917f3dedd44dec2e863ee2854d64c2d5a`. The old-build CalDAV account `Localhost` currently shows its `Journal` collection in Apple Calendar. Its discovery requests returned 207 after the client was configured with the principal URL. I am preparing the seeded area data before switching the same data to the area-calendar build. No code or deployment changes have been made.
Author
Owner

Old-build seed evidence: Apple Calendar imported all five events into the Localhost / Journal calendar. The logging proxy recorded five PUT /dav/calendars/<user>/journal/<uid>.ics responses with HTTP 201, then a REPORT on the same collection with HTTP 207. The visible Calendar view showed Work shift and +4 more on 30 September. The Mac-local destination was not selected. The area upgrade has not yet been started.

Old-build seed evidence: Apple Calendar imported all five events into the `Localhost / Journal` calendar. The logging proxy recorded five `PUT /dav/calendars/<user>/journal/<uid>.ics` responses with HTTP 201, then a `REPORT` on the same collection with HTTP 207. The visible Calendar view showed `Work shift` and `+4 more` on 30 September. The Mac-local destination was not selected. The area upgrade has not yet been started.
Author
Owner

Upgrade-path evidence from the same Localhost account and work4 data after restarting the server at the #356 area-calendar source (28ac39f9, documentation-only changes are on top): Calendar refreshed the collection home and sent PROPFIND to untagged/ and all three stable area-<identity>/ collections; every response was 207. The proxy shows no post-upgrade request to the old journal/ href and no 4xx/5xx response. Calendar’s source tree now lists Commute, Fitness, Untagged, and Work under Localhost; Journal is gone, with no CalDAV error/account alert in the Mac screenshot. AX event rows show each tagged seed in its named calendar and Work and fitness entry in both Work and Fitness. This closes the stale-Journal upgrade risk for this client run. CRUD, color comparison, rename, and Reminders rows remain in progress.

Upgrade-path evidence from the same `Localhost` account and `work4` data after restarting the server at the #356 area-calendar source (`28ac39f9`, documentation-only changes are on top): Calendar refreshed the collection home and sent PROPFIND to `untagged/` and all three stable `area-<identity>/` collections; every response was 207. The proxy shows no post-upgrade request to the old `journal/` href and no 4xx/5xx response. Calendar’s source tree now lists Commute, Fitness, Untagged, and Work under Localhost; Journal is gone, with no CalDAV error/account alert in the Mac screenshot. AX event rows show each tagged seed in its named calendar and `Work and fitness entry` in both Work and Fitness. This closes the stale-Journal upgrade risk for this client run. CRUD, color comparison, rename, and Reminders rows remain in progress.
Author
Owner

Resuming the real Apple client verification. Branch: job/mac-verify; base SHA: 28ac39f917f3dedd44dec2e863ee2854d64c2d5a; resume head: 076cf6d776ded4bab13b790c11ae17de6fa196f5. The previous session refreshed the Mac VM access evidence and recorded that the account sync against the restarted verification server is still pending. I am continuing that upgrade-path check and the remaining requested Reminders cases.

Resuming the real Apple client verification. Branch: `job/mac-verify`; base SHA: `28ac39f917f3dedd44dec2e863ee2854d64c2d5a`; resume head: `076cf6d776ded4bab13b790c11ae17de6fa196f5`. The previous session refreshed the Mac VM access evidence and recorded that the account sync against the restarted verification server is still pending. I am continuing that upgrade-path check and the remaining requested Reminders cases.
Author
Owner

mac-verify result: NO-GO for 28ac39f9 as is. GO for job/mac-verify head 4081a867 (dev + 2 small DAV fixes), after the usual merge gates.

Real Apple Calendar and Reminders, macOS 27 (26A428) VM, account Localhost over TLS via reverse tunnel, logging proxy on every request. Branch job/mac-verify, not pushed, not merged.

Why NO-GO for 28ac39f9

Apple Calendar (dataaccessd) sends MOVE and DELETE without If-Match, and MOVE keeps the source file name in Destination:

MOVE /dav/calendars/<u>/area-ee20…/DA61025F-….ics -> 428
HDRS: Destination: https://localhost:18443/dav/calendars/<u>/area-6d62…/DA61025F-….ics; User-Agent: macOS/27.0 (26A428) dataaccessd/1.0   (no If-Match, no Overwrite)
DELETE /dav/calendars/<u>/area-ee20…/9835BC31-….ics -> 428

Apple never falls back to DELETE+PUT; it retries every sync (log shows 428 from 22:52 to 00:45), shows the move/delete locally, and puts a warning triangle on the account. The server keeps the old Log line: a stuck sync on every calendar move and every delete from Calendar.

Fixes on the branch (with regression replay)

  • 51c6705b area DELETE/MOVE accept a missing If-Match (use the ETag read under the area write lock; the provider write still compares, a present stale If-Match still gets 412). MOVE also accepts the source file name. Replay: apple_calendar_move_and_delete_without_if_match (Apple's exact headers).
  • 33c13168 with only the first fix, MOVE returned 201 but Calendar showed the event twice (its guessed href plus the stable one; cal-before-move2.png), because incremental sync-collection never mentions the guessed href. The accepted MOVE now records the guessed href (10 min, max 128, in process) and the next target-calendar sync REPORT lists it as 404. Known limit: a server restart between MOVE and that REPORT (Apple sends it within 1 s) leaves the duplicate until a full resync.

Re-run on the fixed build: MOVE … -> 201 (00:57:53), REPORT on Fitness returns <href>…/bed38d28….ics</href><status>404</status> + the stable href; Log line Work shift #area/fitness ^35600000-…-0001 (same block ID); one event in Fitness, no alert (cal-after-move2.png). Delete: DELETE -> 204 (00:59:36), Log line gone, kept in the Daily note version.

1. Upgrade path (journal/ → area calendars): PASS

Same account and data: old build served journal/ (5 PUT 201). After switching to 28ac39f9, Calendar re-read the home and PROPFIND/REPORTed untagged/ + 3 area-* (all 207), no request to journal/, no alert. Sidebar: Commute, Fitness, Untagged, Work; Journal gone; the two-area entry shows in Work and Fitness (area-upgrade.png, pre-upgrade.png).

2. Area calendars

Case Result
Create in Work PASS: PUT 201, … Macverify Work create 356 #area/work ^DA61025F…
Move Work → Fitness FAIL on 28ac39f9 (428 forever); PASS on branch (above)
Edit title, edit time PASS: PUT 204, Log line 12:00 - 13:30 … edited #area/work
Delete FAIL on 28ac39f9 (428); PASS on branch (204, file version kept)
Two-area edit via Work PASS: title changed in both copies after REPORT
Colours PASS: DAV and web tagColor equal (Work #48711E, Fitness #445FA7, Commute #8F5300; web-calendar-now.png); Apple sent no PROPPATCH
Rename area/commute → area/travel (tags API) PASS: Apple dropped Commute, added Travel with the event, no duplicate

3. Reminders rows (matrix updated)

  • Tags: not supported for CalDAV lists. #macverify stays plain text in SUMMARY; Apple never sends CATEGORIES (decides #430 T13). calternal keeps it inline in the Task title, so it is indexed as a tag.
  • DTSTART: PASS. Date+time sends DTSTART;TZID=Asia/Calcutta + DUE; stored as scheduled/due, read back.
  • Early reminder: not offered (no Early Reminder row for a CalDAV list).
  • Flag: not offered (no control in inspector or menu).
  • Subtasks: not offered (Indent/Outdent disabled; rem-editmenu.png).
  • Location alert: PASS (custom place, Arriving, 100 m): VALARM with X-APPLE-PROXIMITY + X-APPLE-STRUCTURED-LOCATION stored and read back; Reminders shows "Arriving: Alexanderplatz".
  • URL: not offered (no field).
  • All-day event (Calendar): 422 "property parameters cannot be a Log line" (DESIGN §30 C13, by design); Calendar shows "couldn't be refreshed… 422" with Delete Event. Unchanged from the journal era.
  • Unicode/RTL: PASS (Arabic/Hebrew/CJK/emoji title and notes round-trip, rem1.png).

Findings for follow-up (not blocking)

  • Deleting a Log entry from Calendar also deletes its child lines, including - [ ] → [Task](…) links that Reminders attached to the latest Log entry. Task files stay; links are only in the file version.
  • After a MOVE, Apple PUTs to its guessed href with If-Match (adds VALARM ACTION:NONE); it gets 412, harmless, no alert.
  • Test-harness note: a Python BaseHTTPRequestHandler proxy without do_MOVE returns 501; my earlier proxy did this, so older logs may lack MOVE.

Gates (calternal-dav, after git merge dev)

cargo fmt --check -p calternal-dav: exit 0
cargo clippy -p calternal-dav --all-targets -- -D warnings: Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.47s
cargo test -p calternal-dav:
test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

Full workspace gates not run (only calternal-dav changed; server built and ran with it).

Evidence: artifacts/mac-verify/ in the worktree (screenshots named above); proxy logs scratchpad/macverify/dav-tls.log (summary) and dav.log (bodies + headers, Authorization stripped).

Mac cleanup: Localhost CalDAV account deleted, test root-CA profile removed, helper files removed. One untrusted self-signed localhost cert remains in the login keychain (delete failed over ssh: keychain write permission). Mac lock released.

Also reported on #393 (Reminders rows).

## mac-verify result: **NO-GO for `28ac39f9` as is. GO for `job/mac-verify` head `4081a867`** (dev + 2 small DAV fixes), after the usual merge gates. Real Apple Calendar and Reminders, macOS 27 (26A428) VM, account `Localhost` over TLS via reverse tunnel, logging proxy on every request. Branch `job/mac-verify`, not pushed, not merged. ### Why NO-GO for 28ac39f9 Apple Calendar (dataaccessd) sends **MOVE and DELETE without `If-Match`**, and MOVE keeps the **source file name** in `Destination`: ``` MOVE /dav/calendars/<u>/area-ee20…/DA61025F-….ics -> 428 HDRS: Destination: https://localhost:18443/dav/calendars/<u>/area-6d62…/DA61025F-….ics; User-Agent: macOS/27.0 (26A428) dataaccessd/1.0 (no If-Match, no Overwrite) DELETE /dav/calendars/<u>/area-ee20…/9835BC31-….ics -> 428 ``` Apple never falls back to DELETE+PUT; it retries every sync (log shows 428 from 22:52 to 00:45), shows the move/delete locally, and puts a warning triangle on the account. The server keeps the old Log line: a stuck sync on every calendar move and every delete from Calendar. ### Fixes on the branch (with regression replay) - `51c6705b` area DELETE/MOVE accept a missing `If-Match` (use the ETag read under the area write lock; the provider write still compares, a present stale `If-Match` still gets 412). MOVE also accepts the source file name. Replay: `apple_calendar_move_and_delete_without_if_match` (Apple's exact headers). - `33c13168` with only the first fix, MOVE returned 201 but Calendar showed the event **twice** (its guessed href plus the stable one; `cal-before-move2.png`), because incremental sync-collection never mentions the guessed href. The accepted MOVE now records the guessed href (10 min, max 128, in process) and the next target-calendar sync REPORT lists it as 404. Known limit: a server restart between MOVE and that REPORT (Apple sends it within 1 s) leaves the duplicate until a full resync. Re-run on the fixed build: `MOVE … -> 201` (00:57:53), REPORT on Fitness returns `<href>…/bed38d28….ics</href><status>404</status>` + the stable href; Log line `Work shift #area/fitness ^35600000-…-0001` (same block ID); one event in Fitness, no alert (`cal-after-move2.png`). Delete: `DELETE -> 204` (00:59:36), Log line gone, kept in the Daily note version. ### 1. Upgrade path (journal/ → area calendars): PASS Same account and data: old build served `journal/` (5 PUT 201). After switching to 28ac39f9, Calendar re-read the home and PROPFIND/REPORTed `untagged/` + 3 `area-*` (all 207), **no request to `journal/`**, no alert. Sidebar: Commute, Fitness, Untagged, Work; Journal gone; the two-area entry shows in Work and Fitness (`area-upgrade.png`, `pre-upgrade.png`). ### 2. Area calendars | Case | Result | |---|---| | Create in Work | PASS: PUT 201, `… Macverify Work create 356 #area/work ^DA61025F…` | | Move Work → Fitness | FAIL on 28ac39f9 (428 forever); PASS on branch (above) | | Edit title, edit time | PASS: PUT 204, Log line `12:00 - 13:30 … edited #area/work` | | Delete | FAIL on 28ac39f9 (428); PASS on branch (204, file version kept) | | Two-area edit via Work | PASS: title changed in both copies after REPORT | | Colours | PASS: DAV and web tagColor equal (Work #48711E, Fitness #445FA7, Commute #8F5300; `web-calendar-now.png`); Apple sent no PROPPATCH | | Rename area/commute → area/travel (tags API) | PASS: Apple dropped Commute, added Travel with the event, no duplicate | ### 3. Reminders rows (matrix updated) - Tags: **not supported for CalDAV lists**. `#macverify` stays plain text in SUMMARY; Apple never sends CATEGORIES (decides #430 T13). calternal keeps it inline in the Task title, so it is indexed as a tag. - DTSTART: PASS. Date+time sends `DTSTART;TZID=Asia/Calcutta` + `DUE`; stored as `scheduled`/`due`, read back. - Early reminder: **not offered** (no Early Reminder row for a CalDAV list). - Flag: **not offered** (no control in inspector or menu). - Subtasks: **not offered** (Indent/Outdent disabled; `rem-editmenu.png`). - Location alert: PASS (custom place, Arriving, 100 m): VALARM with `X-APPLE-PROXIMITY` + `X-APPLE-STRUCTURED-LOCATION` stored and read back; Reminders shows "Arriving: Alexanderplatz". - URL: **not offered** (no field). - All-day event (Calendar): 422 "property parameters cannot be a Log line" (DESIGN §30 C13, by design); Calendar shows "couldn't be refreshed… 422" with Delete Event. Unchanged from the journal era. - Unicode/RTL: PASS (Arabic/Hebrew/CJK/emoji title and notes round-trip, `rem1.png`). ### Findings for follow-up (not blocking) - Deleting a Log entry from Calendar also deletes its child lines, including `- [ ] → [Task](…)` links that Reminders attached to the latest Log entry. Task files stay; links are only in the file version. - After a MOVE, Apple PUTs to its guessed href with `If-Match` (adds `VALARM ACTION:NONE`); it gets 412, harmless, no alert. - Test-harness note: a Python `BaseHTTPRequestHandler` proxy without `do_MOVE` returns 501; my earlier proxy did this, so older logs may lack MOVE. ### Gates (calternal-dav, after `git merge dev`) ``` cargo fmt --check -p calternal-dav: exit 0 cargo clippy -p calternal-dav --all-targets -- -D warnings: Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.47s cargo test -p calternal-dav: test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s ``` Full workspace gates not run (only calternal-dav changed; server built and ran with it). Evidence: `artifacts/mac-verify/` in the worktree (screenshots named above); proxy logs `scratchpad/macverify/dav-tls.log` (summary) and `dav.log` (bodies + headers, Authorization stripped). Mac cleanup: Localhost CalDAV account deleted, test root-CA profile removed, helper files removed. One untrusted self-signed `localhost` cert remains in the login keychain (delete failed over ssh: keychain write permission). Mac lock released. Also reported on #393 (Reminders rows).
Author
Owner

Merged into dev at 06b1b5c73 and pushed. Deploy status is on #203.

Merged into dev at 06b1b5c73 and pushed. Deploy status is on #203.
kayg closed this issue 2026-09-30 00:29: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#356
No description provided.