Calendar: rich task counted twice (→ bullet + task file checkbox); +N chip is a no-op #357

Closed
opened 2026-09-28 15:42:35 +00:00 by kayg · 10 comments
Owner

Bug (owner, 2026-09-28, production)

Calendar Day view all-day row shows "☐ Buy milk" and a "+1" chip although the day has one task. Clicking "+1" does nothing.

Root cause (Claude, from production files)

One task is counted twice for 2026-09-28:

  1. the attachment bullet in the daily note: - [ ] → [Buy milk](Notes/20260928-buy-milk-<id>.md)
  2. the checkbox in the rich task file body: Notes/20260928-buy-milk-<id>.md has frontmatter calternal-id, title: Buy milk, status: todo, created: 2026-09-28 and body - [ ] Buy milk.

Both reach the day's tasks list, so chips = allDay.length + tasks.length = 2 (packages/ui/src/components/calendar/TimeGrid.svelte ~line 916).

Fix

  • One task, one identity (DESIGN §31 task model: rich task file first, attached by a → link bullet). The → bullet and the task file are the same task, keyed by the task file's calternal-id. The body checkbox that mirrors the title in a rich task file is not a second task. Fix this where the day's task list is built (server/index), not by de-duplicating in the component, so Calendar, the task sidebar, search, CalDAV VTODO (reminders) and the CLI all agree. Check each of those for the same double count.
  • Check whether the task file's own body checkbox and the frontmatter status can diverge; the frontmatter status is authoritative per §31. Toggling either must keep one state.
  • "+N" chip: in Day view onopenday(date) opens the day that is already open, so the button is a no-op. Replace it in every view with an anchored popover listing all all-day items for that day (same preview surface as #303), keyboard and screen-reader operable. A control that does nothing is a defect.
  • Regression tests: a day with one rich task attached via → has exactly one task in the day API, the calendar, the VTODO collection and calternal tasks --json; "+N" opens the list at 390/820/1440.

Screenshots at 390/820/1440 light and dark attached by the job; Claude reviews.

## Bug (owner, 2026-09-28, production) Calendar Day view all-day row shows "☐ Buy milk" and a "+1" chip although the day has one task. Clicking "+1" does nothing. ## Root cause (Claude, from production files) One task is counted twice for 2026-09-28: 1. the attachment bullet in the daily note: ` - [ ] → [Buy milk](Notes/20260928-buy-milk-<id>.md)` 2. the checkbox in the rich task file body: `Notes/20260928-buy-milk-<id>.md` has frontmatter `calternal-id`, `title: Buy milk`, `status: todo`, `created: 2026-09-28` and body `- [ ] Buy milk`. Both reach the day's `tasks` list, so `chips = allDay.length + tasks.length` = 2 (`packages/ui/src/components/calendar/TimeGrid.svelte` ~line 916). ## Fix - One task, one identity (DESIGN §31 task model: rich task file first, attached by a `→` link bullet). The `→` bullet and the task file are the same task, keyed by the task file's calternal-id. The body checkbox that mirrors the title in a rich task file is not a second task. Fix this where the day's task list is built (server/index), not by de-duplicating in the component, so Calendar, the task sidebar, search, CalDAV VTODO (reminders) and the CLI all agree. Check each of those for the same double count. - Check whether the task file's own body checkbox and the frontmatter `status` can diverge; the frontmatter status is authoritative per §31. Toggling either must keep one state. - "+N" chip: in Day view `onopenday(date)` opens the day that is already open, so the button is a no-op. Replace it in every view with an anchored popover listing all all-day items for that day (same preview surface as #303), keyboard and screen-reader operable. A control that does nothing is a defect. - Regression tests: a day with one rich task attached via `→` has exactly one task in the day API, the calendar, the VTODO collection and `calternal tasks --json`; "+N" opens the list at 390/820/1440. Screenshots at 390/820/1440 light and dark attached by the job; Claude reviews.
Author
Owner

Starting work on job/dup-task, based on dev at fcba3cb1092eaae0c5c3162b8b9b0a532b40f43a.

Starting work on `job/dup-task`, based on `dev` at `fcba3cb1092eaae0c5c3162b8b9b0a532b40f43a`.
Author
Owner

The new Notes API regression reproduced the duplicate at the Index source: indexing one attached rich Task plus its Daily note produced 2 task_items rows (expected 1), and /tasks/day exposed 2 items. The root checkbox and the → reference both described the File Task. The source projector now stores one row; the same router test returns one standalone Task and one day Task. It also found that ticking changed frontmatter to status: done while leaving the mirror as - [ ]; the writer now updates both in one file replacement.

The new Notes API regression reproduced the duplicate at the Index source: indexing one attached rich Task plus its Daily note produced 2 `task_items` rows (expected 1), and `/tasks/day` exposed 2 items. The root checkbox and the `→` reference both described the File Task. The source projector now stores one row; the same router test returns one standalone Task and one day Task. It also found that ticking changed frontmatter to `status: done` while leaving the mirror as `- [ ]`; the writer now updates both in one file replacement.
Author
Owner

Evidence update for the rich Task projection:

The new regression test reproduced data loss in extract_task_index: a rich Task with frontmatter title Buy milk and first child Refill pantry ^child123 produced one row, dropping that child. The extractor now skips only an unanchored checkbox whose parsed title matches the Task title. A shared core writer applies status markers only to that matching mirror and leaves child-only legacy files unchanged.

The existing round_trip_extract_recovers_all_fields assertion expected two rows (File plus the root checkbox). I changed it to one File row because #357 explicitly requires the rich Task and its Markdown mirror to have one identity. The focused core suite is rerunning with that behavior.

Evidence update for the rich Task projection: The new regression test reproduced data loss in `extract_task_index`: a rich Task with frontmatter title `Buy milk` and first child `Refill pantry ^child123` produced one row, dropping that child. The extractor now skips only an unanchored checkbox whose parsed title matches the Task title. A shared core writer applies status markers only to that matching mirror and leaves child-only legacy files unchanged. The existing `round_trip_extract_recovers_all_fields` assertion expected two rows (File plus the root checkbox). I changed it to one File row because #357 explicitly requires the rich Task and its Markdown mirror to have one identity. The focused core suite is rerunning with that behavior.
Author
Owner

Resuming job/dup-task at ea4b0a0a after the build host terminated the prior run for disk exhaustion. I preserved the existing committed slices and uncommitted changes. The original base SHA is recorded above (fcba3cb1092eaae0c5c3162b8b9b0a532b40f43a); I will merge current dev once before final gates as requested.

Resuming `job/dup-task` at `ea4b0a0a` after the build host terminated the prior run for disk exhaustion. I preserved the existing committed slices and uncommitted changes. The original base SHA is recorded above (`fcba3cb1092eaae0c5c3162b8b9b0a532b40f43a`); I will merge current `dev` once before final gates as requested.
Author
Owner

Finding from route review after the focused Notes test passed: PUT /api/v1/notes/{id}/body writes the Task Markdown and rebuilds its projection, but it does not reconcile a changed root checkbox with frontmatter status. Also, PATCH /api/v1/notes/tasks/properties can rename the frontmatter title while leaving the root mirror title stale, so a later status change no longer matches that mirror. I am adding route regressions and will use the existing root-mirror writer for both paths.

Finding from route review after the focused Notes test passed: `PUT /api/v1/notes/{id}/body` writes the Task Markdown and rebuilds its projection, but it does not reconcile a changed root checkbox with frontmatter `status`. Also, `PATCH /api/v1/notes/tasks/properties` can rename the frontmatter title while leaving the root mirror title stale, so a later status change no longer matches that mirror. I am adding route regressions and will use the existing root-mirror writer for both paths.
Author
Owner

The Notes regression now covers the full rich Task path: one Index row, one /tasks/day result, one /tasks collection result, one CalDAV VTODO, synchronized tick/status writes, title rename, and a root-checkbox edit through the Note body route. The body and frontmatter regression passed:

test tests::attached_rich_task_has_one_day_identity_and_mirrored_status ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 104 filtered out; finished in 1.02s

The related source slice is committed as a48e099f.

The Notes regression now covers the full rich Task path: one Index row, one `/tasks/day` result, one `/tasks` collection result, one CalDAV VTODO, synchronized `tick`/status writes, title rename, and a root-checkbox edit through the Note body route. The body and frontmatter regression passed: `test tests::attached_rich_task_has_one_day_identity_and_mirrored_status ... ok` `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 104 filtered out; finished in 1.02s` The related source slice is committed as `a48e099f`.
Author
Owner

Focused regression evidence after the server slice:

  • cargo test -p calternal-search --test indexer rich_task_with_daily_note_attachment_has_one_task_search_identity: test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 17 filtered out; finished in 1.40s
  • cargo test -p calternal-cli tasks_collects_every_page_into_one_json_array: test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 11 filtered out; finished in 0.01s
  • bun run test -- src/lib/calendar/TimeGrid.svelte.test.ts: Test Files 1 passed (1) and Tests 4 passed (4)

Commits: f5b892b1 adds the rich Task search identity regression; 97eeba95 adds the production Calendar screenshot runner for Day and Week at 390/820/1440 in light and dark, with icon alignment polish. I am proceeding to the required local dev merge and final evidence.

Focused regression evidence after the server slice: - `cargo test -p calternal-search --test indexer rich_task_with_daily_note_attachment_has_one_task_search_identity`: `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 17 filtered out; finished in 1.40s` - `cargo test -p calternal-cli tasks_collects_every_page_into_one_json_array`: `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 11 filtered out; finished in 0.01s` - `bun run test -- src/lib/calendar/TimeGrid.svelte.test.ts`: `Test Files 1 passed (1)` and `Tests 4 passed (4)` Commits: `f5b892b1` adds the rich Task search identity regression; `97eeba95` adds the production Calendar screenshot runner for Day and Week at 390/820/1440 in light and dark, with icon alignment polish. I am proceeding to the required local `dev` merge and final evidence.
Author
Owner

Implemented Forgejo #357 and pushed job/dup-task.

What changed

A rich Task now keeps one canonical indexed identity across its root File Task, Daily note attachment, day API, search, CalDAV VTODO, and CLI. Root checkbox, title, and status edits stay synchronized. The Notes API now provides paginated task listing for CLI consumers. The calendar +N all-day chip opens the existing anchored popover/sheet with the full item list in Day and Week views.

Files

  • crates/calternal-notes-core/src/tasks/{build.rs,extract.rs,line.rs,mod.rs}
  • crates/plugins/notes/src/{lib.rs,tasks_api.rs,tasks_dav.rs,tasks_store.rs}
  • crates/calternal-cli/src/{main.rs,remote_commands.rs}
  • crates/calternal-search/tests/indexer.rs
  • packages/ui/src/components/calendar/TimeGrid.svelte and its test
  • apps/web/e2e/calendar-task-overflow.mjs, apps/web/package.json
  • tests/adversarial/attack.py

Verification

cargo fmt --check passed (exit 0, no output).

bun run check passed. Output:

Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/dup-task/apps/web
Getting Svelte diagnostics...
svelte-check found 0 errors and 0 warnings

bun run test passed. Output:

Test Files  113 passed (113)
Tests  747 passed (747)
Duration 291.90s

bun run build passed:

✓ built in 58.63s
Wrote site to "build"
✔ done

The focused Rust tests passed: calternal-notes-core (494 tests), the Notes Task identity/status route regression, Search deduplication, and CLI pagination. The full workspace cargo clippy --all-targets -- -D warnings did not complete within the four-hour job window; it was interrupted with exit 130 while checking dependencies, with no lint diagnostic reported before interruption. The full workspace cargo test was not run. Cargo verification used OPENSSL_NO_VENDOR=1 with installed OpenSSL 3.5.7 because the vendored OpenSSL build exceeded the job window.

The one time-boxed adversarial round completed with exit 0. Output:

finish 200
login web 200 installation 200
cookies [ "__Host-calternal_session secure=true httpOnly=true sameSite=Lax" ]
invite 200
second user finish 200
fixture passkey login requests: 30/30
DAV home listing concurrency: 7/24 returned expected rate_limited backpressure
DAV Apple property, write-capability, MKCALENDAR and adversarial probes completed

The production E2E run captured 12 screenshots: Day and Week at 390, 820, and 1440 px in light and dark themes. They are attached below. The last source change only captured the selected date in an event callback; the production build used for the screenshots preceded that small fix. Post-fix bun run check and bun run test passed. cargo clean removed 8.9 GiB and web build output was deleted.

Screenshot attachments

Decisions for owner confirmation

  • The anchored root File Task remains the canonical task identity. A Daily note attachment is a reference to it and does not create a second indexed task.
  • The new task list API uses cursor pagination, and the CLI collects its pages into one result.
  • The mobile all-day overflow uses the shared surface as a sheet and shows one heading for the list.
  • Local Cargo verification used system OpenSSL 3.5.7 via OPENSSL_NO_VENDOR=1 to fit the job window; project dependency configuration was not changed.

Head: ce888dffe91c505c2b1979cec5d1ba8f8f1c9716

Implemented Forgejo #357 and pushed `job/dup-task`. ## What changed A rich Task now keeps one canonical indexed identity across its root File Task, Daily note attachment, day API, search, CalDAV VTODO, and CLI. Root checkbox, title, and status edits stay synchronized. The Notes API now provides paginated task listing for CLI consumers. The calendar `+N` all-day chip opens the existing anchored popover/sheet with the full item list in Day and Week views. ## Files - `crates/calternal-notes-core/src/tasks/{build.rs,extract.rs,line.rs,mod.rs}` - `crates/plugins/notes/src/{lib.rs,tasks_api.rs,tasks_dav.rs,tasks_store.rs}` - `crates/calternal-cli/src/{main.rs,remote_commands.rs}` - `crates/calternal-search/tests/indexer.rs` - `packages/ui/src/components/calendar/TimeGrid.svelte` and its test - `apps/web/e2e/calendar-task-overflow.mjs`, `apps/web/package.json` - `tests/adversarial/attack.py` ## Verification `cargo fmt --check` passed (exit 0, no output). `bun run check` passed. Output: ```text Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/dup-task/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test` passed. Output: ```text Test Files 113 passed (113) Tests 747 passed (747) Duration 291.90s ``` `bun run build` passed: ```text ✓ built in 58.63s Wrote site to "build" ✔ done ``` The focused Rust tests passed: `calternal-notes-core` (494 tests), the Notes Task identity/status route regression, Search deduplication, and CLI pagination. The full workspace `cargo clippy --all-targets -- -D warnings` did not complete within the four-hour job window; it was interrupted with exit 130 while checking dependencies, with no lint diagnostic reported before interruption. The full workspace `cargo test` was not run. Cargo verification used `OPENSSL_NO_VENDOR=1` with installed OpenSSL 3.5.7 because the vendored OpenSSL build exceeded the job window. The one time-boxed adversarial round completed with exit 0. Output: ```text finish 200 login web 200 installation 200 cookies [ "__Host-calternal_session secure=true httpOnly=true sameSite=Lax" ] invite 200 second user finish 200 fixture passkey login requests: 30/30 DAV home listing concurrency: 7/24 returned expected rate_limited backpressure DAV Apple property, write-capability, MKCALENDAR and adversarial probes completed ``` The production E2E run captured 12 screenshots: Day and Week at 390, 820, and 1440 px in light and dark themes. They are attached below. The last source change only captured the selected date in an event callback; the production build used for the screenshots preceded that small fix. Post-fix `bun run check` and `bun run test` passed. `cargo clean` removed 8.9 GiB and web build output was deleted. ## Screenshot attachments - Day: [390 light](https://git.kayg.org/attachments/18364d56-1349-47c8-ba9b-8e6372865a37), [390 dark](https://git.kayg.org/attachments/e2dab1c8-1f2d-486b-bdb8-457b33906121), [820 light](https://git.kayg.org/attachments/2627739e-a459-4cb8-92cf-1aa98a4c8506), [820 dark](https://git.kayg.org/attachments/78fe4d92-601d-4ea1-9b1d-3a9076b1850f), [1440 light](https://git.kayg.org/attachments/dfa54963-57f8-4e8b-a87d-4e0f15296037), [1440 dark](https://git.kayg.org/attachments/da892b7b-87a2-42ee-be92-1e249d521610) - Week: [390 light](https://git.kayg.org/attachments/8c60a623-b976-418e-953a-221ba8ba1ef8), [390 dark](https://git.kayg.org/attachments/34b2c00f-68de-4de2-80ee-51e9337743dd), [820 light](https://git.kayg.org/attachments/49cd5007-9b73-435a-97ba-da8b8df10d5d), [820 dark](https://git.kayg.org/attachments/616aae76-908b-4f0f-b96d-38d385b0ad70), [1440 light](https://git.kayg.org/attachments/0b64be6f-6f3a-45b8-a74c-2ed4a4d651dc), [1440 dark](https://git.kayg.org/attachments/9aed09df-20e9-4f20-8709-3eaee3e1339c) ## Decisions for owner confirmation - The anchored root File Task remains the canonical task identity. A Daily note attachment is a reference to it and does not create a second indexed task. - The new task list API uses cursor pagination, and the CLI collects its pages into one result. - The mobile all-day overflow uses the shared surface as a sheet and shows one heading for the list. - Local Cargo verification used system OpenSSL 3.5.7 via `OPENSSL_NO_VENDOR=1` to fit the job window; project dependency configuration was not changed. Head: `ce888dffe91c505c2b1979cec5d1ba8f8f1c9716`
Author
Owner

Verification clarification: the calternal-notes-core suite with 494 tests passed before the resumed edits. After those edits, the focused root-title mirror regression passed. The full workspace cargo test gate was not run, as stated above.

Verification clarification: the `calternal-notes-core` suite with 494 tests passed before the resumed edits. After those edits, the focused root-title mirror regression passed. The full workspace `cargo test` gate was not run, as stated above.
Author
Owner

Merged into dev by Claude after review (413ccaa7), deploying to calternal.cloud. Closing.

Merged into dev by Claude after review (413ccaa7), deploying to calternal.cloud. Closing.
kayg closed this issue 2026-09-28 23:46:21 +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#357
No description provided.