Web clipper and bookmarks that show up in the calendar #51

Closed
opened 2026-09-24 15:27:02 +00:00 by kayg · 11 comments
Owner

Owner decision (PKM round, K8): "absolutely essential; having bookmarks show up in the calendar is awesome for discoverability".

  • A browser extension (Chromium + Firefox + Safari via the web-extension format) and a bookmarklet fallback: save the current page as a bookmark (URL, title, favicon, description, optional selection/highlights) or as a clipped note (readable article converted to Markdown with Readability, images downloaded into Attachments/).
  • Storage, file over app: each bookmark/clip is a Markdown note in Notes/ (flat, calternal note identity) with properties url, site, saved, kind: bookmark|clip, tags; highlights as blockquotes.
  • Calendar: bookmarks appear in the calendar activity lane on the day saved (stacked when many, favicon thumbnails, hover preview with title/site/description) — #39.
  • Search: ⌘K finds bookmarks by title, site, tags, and clipped text.
  • Auth: per-installation token; the extension is a first-party client using the typed API client.

Context for the owning job

  • Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§9, §17, §18, §29–§31) first.
  • Owner rules: file over app (plain Markdown/files are the truth; the DB is a rebuildable index); the server is the single writer; data loss is unacceptable; performance first but never at the cost of finesse; UI in the calternal.js design system (Claude reviews screenshots); never ship sample/mock data; atomic commits; adversarial testing after API work; good enough, not perfect.
  • Comment on this issue when you start (branch, base SHA), on each finding, when blocked, and when finished (head SHA + gate output). Never close it.
Owner decision (PKM round, K8): "absolutely essential; having bookmarks show up in the calendar is awesome for discoverability". - A browser extension (Chromium + Firefox + Safari via the web-extension format) and a bookmarklet fallback: save the current page as a bookmark (URL, title, favicon, description, optional selection/highlights) or as a clipped note (readable article converted to Markdown with Readability, images downloaded into Attachments/). - Storage, file over app: each bookmark/clip is a Markdown note in `Notes/` (flat, calternal note identity) with properties `url`, `site`, `saved`, `kind: bookmark|clip`, tags; highlights as blockquotes. - Calendar: bookmarks appear in the calendar activity lane on the day saved (stacked when many, favicon thumbnails, hover preview with title/site/description) — #39. - Search: ⌘K finds bookmarks by title, site, tags, and clipped text. - Auth: per-installation token; the extension is a first-party client using the typed API client. ## Context for the owning job - Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§9, §17, §18, §29–§31) first. - Owner rules: file over app (plain Markdown/files are the truth; the DB is a rebuildable index); the server is the single writer; data loss is unacceptable; performance first but never at the cost of finesse; UI in the calternal.js design system (Claude reviews screenshots); never ship sample/mock data; atomic commits; adversarial testing after API work; good enough, not perfect. - Comment on this issue when you start (branch, base SHA), on each finding, when blocked, and when finished (head SHA + gate output). Never close it.
Author
Owner

Started bookmarks work on branch job/bookmarks at base 6c943d6f16.

Started bookmarks work on branch job/bookmarks at base 6c943d6f16df6b75e9a79b68423a0e0d8c67f640.
Author
Owner

Finding: calendar/src/view.rs currently groups Notes by files_index.modified, and note_items only projects kind. Retitle or edit can move a saved bookmark to a later day, and the range cannot return the requested site/description/favicon preview data. I will add migration 0007 for rebuildable Note projections from Markdown, then group bookmark entries by their stable saved property and return those preview fields. Markdown remains authoritative.

Finding: calendar/src/view.rs currently groups Notes by files_index.modified, and note_items only projects kind. Retitle or edit can move a saved bookmark to a later day, and the range cannot return the requested site/description/favicon preview data. I will add migration 0007 for rebuildable Note projections from Markdown, then group bookmark entries by their stable saved property and return those preview fields. Markdown remains authoritative.
Author
Owner

Calendar query follow-up: load_file_and_note_rows filters files_index.modified before it projects Note rows. A bookmark edited after its saved date would be absent from that earlier range even if its day were calculated from saved. I will add a derived saved_epoch column and include rows whose bookmark save time falls inside the requested window, then group and display by that timestamp.

Calendar query follow-up: load_file_and_note_rows filters files_index.modified before it projects Note rows. A bookmark edited after its saved date would be absent from that earlier range even if its day were calculated from saved. I will add a derived saved_epoch column and include rows whose bookmark save time falls inside the requested window, then group and display by that timestamp.
Author
Owner

Search finding: the Notes search provider matched title and tags but omitted the site property. The existing search Index already indexes Markdown files in the Home, so I added a site projection to Notes search; clip text remains searchable through the existing full-text Index.

Search finding: the Notes search provider matched title and tags but omitted the site property. The existing search Index already indexes Markdown files in the Home, so I added a site projection to Notes search; clip text remains searchable through the existing full-text Index.
Author
Owner

Adversarial review found that captured article images were trusted by their Content-Type alone, so a server could return active SVG or HTML labeled image/png. I added signature checks for JPEG, PNG, GIF, WebP, and AVIF before attachments are written. Regression: cargo test -p calternal-plugin-notes --lib bookmarks::tests::downloaded_images_need_a_safe_type_and_matching_raster_signature passes.

Adversarial review found that captured article images were trusted by their `Content-Type` alone, so a server could return active SVG or HTML labeled `image/png`. I added signature checks for JPEG, PNG, GIF, WebP, and AVIF before attachments are written. Regression: `cargo test -p calternal-plugin-notes --lib bookmarks::tests::downloaded_images_need_a_safe_type_and_matching_raster_signature` passes.
Author
Owner

The new regression test showed that NotesSearch returned no result for a term present only in a clipped article body (0 hits, expected 1). The provider currently searches title, site and tags. I am adding a derived full-text projection for clip bodies, sourced from Markdown during index/reconciliation, with a user-scoped search regression.

The new regression test showed that `NotesSearch` returned no result for a term present only in a clipped article body (0 hits, expected 1). The provider currently searches title, site and tags. I am adding a derived full-text projection for clip bodies, sourced from Markdown during index/reconciliation, with a user-scoped search regression.
Author
Owner

The required cargo test --workspace run fails in calternal-server's wire::tests::live_apps_run_in_separate_processes child test after 758.63 seconds. The captured child output contains only the test-start line, with no panic or assertion. The bookmark and clip Notes tests completed successfully earlier in the run. I am isolating the live-app failure; no out-of-scope code has been changed.

The required `cargo test --workspace` run fails in `calternal-server`'s `wire::tests::live_apps_run_in_separate_processes` child test after 758.63 seconds. The captured child output contains only the test-start line, with no panic or assertion. The bookmark and clip Notes tests completed successfully earlier in the run. I am isolating the live-app failure; no out-of-scope code has been changed.
Author
Owner

The required live run reached the new bookmark section without reporting a bookmark or clip finding. The overall tests/adversarial/run.sh gate failed in pre-existing probes: round one reported 31 findings, including 7 timed-out requests in the existing 24-way Task storm and a 7.2s DAV response; round two reported 14 findings, including attack2.py user-archive operations returning 403 and one shared-folder listing miss. Three full adversarial runners and several Cargo builds were active concurrently in this shared environment. I have not changed out-of-scope endpoints or suppressed these findings.

The required live run reached the new bookmark section without reporting a bookmark or clip finding. The overall `tests/adversarial/run.sh` gate failed in pre-existing probes: round one reported 31 findings, including 7 timed-out requests in the existing 24-way Task storm and a 7.2s DAV response; round two reported 14 findings, including `attack2.py` user-archive operations returning 403 and one shared-folder listing miss. Three full adversarial runners and several Cargo builds were active concurrently in this shared environment. I have not changed out-of-scope endpoints or suppressed these findings.
Author
Owner

Finished bookmarks work.

Head SHA: d462a3a1167874bbc3c4a89ff1e00ac27a5f2719.

Gate output:

  • cargo fmt --check — exit 0, no output.
  • cargo clippy --workspace --all-targets -- -D warnings:
       Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/bookmarks/crates/calternal-server)
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.48s
    
  • cargo test --workspace:
    test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.98s
    test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.40s
    test wire::tests::live_apps_run_in_separate_processes has been running for over 60 seconds
    test wire::tests::live_apps_run_in_separate_processes ... FAILED
    test result: FAILED. 13 passed; 1 failed; 2 ignored; 0 measured; 0 filtered out; finished in 758.63s
    error: test failed, to rerun pass `-p calternal-server --bin calternal-server`
    
    The failing child output contained only test wire::tests::full_app_setup_session_config_and_backup ...; no panic or assertion text followed.
  • bash packages/api-client/check-generated.sh — exit 0. Output ended with:
    ✨ openapi-typescript 7.13.0
    🚀 ../../contracts/openapi.json → src/generated.ts [879.5ms]
    
  • bash tests/adversarial/run.sh — failed. The new bookmark/clip probes reported no findings. Existing probes reported:
    ==== FINDINGS 31
    - Task storm 7 :: NO RESPONSE (b'timed out')
    - DAV Basic app password :: SLOW 7.2s status 207
    ==== ROUND 2 FINDINGS 14
    - list shared folder :: unindexed child missing from direct listing
    - archive user Home :: expected 204, got 403 b''
    
    Other Task storm requests took 5.3–27.1 seconds. Three adversarial runners and several Cargo builds were active on the shared host during this run.
  • cargo clean:
         Removed 20938 files, 13.1GiB total
    

Decisions not covered by the design:

  • Use POST /api/v1/notes/bookmarks; kind defaults to bookmark. A clip uses kind: "clip".
  • Store a supplied favicon HTTP(S) URL in the Note without fetching it. Use the supplied title, then the extracted page title, then the site host.
  • For clips, allow only HTTP(S) on ports 80 and 443. Pin each public DNS answer and recheck every redirect. Limit one capture to 25 seconds, 5 MiB of HTML, 2 MiB of Markdown, eight raster images at 1 MiB each, and two concurrent clip fetches.
  • Accept JPEG, PNG, GIF, WebP and AVIF images only when MIME and byte signature match. Index capture bodies in a rebuildable FTS table, up to 2 MiB per body.

The implementation, generated contract, and probe are committed in six atomic commits. The complete gate gaps above remain visible for review; I did not change unrelated endpoints or suppress their findings.

Finished bookmarks work. Head SHA: `d462a3a1167874bbc3c4a89ff1e00ac27a5f2719`. Gate output: - `cargo fmt --check` — exit 0, no output. - `cargo clippy --workspace --all-targets -- -D warnings`: ``` Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/bookmarks/crates/calternal-server) Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.48s ``` - `cargo test --workspace`: ``` test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.98s test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.40s test wire::tests::live_apps_run_in_separate_processes has been running for over 60 seconds test wire::tests::live_apps_run_in_separate_processes ... FAILED test result: FAILED. 13 passed; 1 failed; 2 ignored; 0 measured; 0 filtered out; finished in 758.63s error: test failed, to rerun pass `-p calternal-server --bin calternal-server` ``` The failing child output contained only `test wire::tests::full_app_setup_session_config_and_backup ...`; no panic or assertion text followed. - `bash packages/api-client/check-generated.sh` — exit 0. Output ended with: ``` ✨ openapi-typescript 7.13.0 🚀 ../../contracts/openapi.json → src/generated.ts [879.5ms] ``` - `bash tests/adversarial/run.sh` — failed. The new bookmark/clip probes reported no findings. Existing probes reported: ``` ==== FINDINGS 31 - Task storm 7 :: NO RESPONSE (b'timed out') - DAV Basic app password :: SLOW 7.2s status 207 ==== ROUND 2 FINDINGS 14 - list shared folder :: unindexed child missing from direct listing - archive user Home :: expected 204, got 403 b'' ``` Other Task storm requests took 5.3–27.1 seconds. Three adversarial runners and several Cargo builds were active on the shared host during this run. - `cargo clean`: ``` Removed 20938 files, 13.1GiB total ``` Decisions not covered by the design: - Use `POST /api/v1/notes/bookmarks`; `kind` defaults to `bookmark`. A clip uses `kind: "clip"`. - Store a supplied favicon HTTP(S) URL in the Note without fetching it. Use the supplied title, then the extracted page title, then the site host. - For clips, allow only HTTP(S) on ports 80 and 443. Pin each public DNS answer and recheck every redirect. Limit one capture to 25 seconds, 5 MiB of HTML, 2 MiB of Markdown, eight raster images at 1 MiB each, and two concurrent clip fetches. - Accept JPEG, PNG, GIF, WebP and AVIF images only when MIME and byte signature match. Index capture bodies in a rebuildable FTS table, up to 2 MiB per body. The implementation, generated contract, and probe are committed in six atomic commits. The complete gate gaps above remain visible for review; I did not change unrelated endpoints or suppress their findings.
Author
Owner

Follow-up to my completion comment: after the full workspace gate failure, I attempted the failing full_app_setup_session_config_and_backup test directly. It compiled successfully, then remained inside the test without further output for 4m57s. At the time, two copies of that same server test were still running in other worktrees (observed elapsed times 8m24s and 16m57s), so this was not an isolated reproduction. I interrupted only my diagnostic run (exit 130), ran cargo clean (Removed 6623 files, 3.5GiB total), and confirmed the branch remains at d462a3a1167874bbc3c4a89ff1e00ac27a5f2719 with a clean worktree. The original cargo test --workspace result and adversarial findings remain unchanged; this diagnostic does not resolve either gate.

Follow-up to my completion comment: after the full workspace gate failure, I attempted the failing `full_app_setup_session_config_and_backup` test directly. It compiled successfully, then remained inside the test without further output for 4m57s. At the time, two copies of that same server test were still running in other worktrees (observed elapsed times 8m24s and 16m57s), so this was not an isolated reproduction. I interrupted only my diagnostic run (exit 130), ran `cargo clean` (`Removed 6623 files, 3.5GiB total`), and confirmed the branch remains at `d462a3a1167874bbc3c4a89ff1e00ac27a5f2719` with a clean worktree. The original `cargo test --workspace` result and adversarial findings remain unchanged; this diagnostic does not resolve either gate.
Author
Owner

Completed on dev in 9ea39cfbac (Merge job/bookmarks: bookmarks and clips as notes, SSRF-safe clipping, calendar + search (#51)).

Completed on dev in 9ea39cfbace33e836a7c4dd9d870c7da33d20e88 (Merge job/bookmarks: bookmarks and clips as notes, SSRF-safe clipping, calendar + search (#51)).
kayg closed this issue 2026-10-01 05:08:50 +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#51
No description provided.