Rich link cards in the block editor #1151

Open
opened 2026-10-05 15:14:51 +00:00 by kayg · 19 comments
Owner

"Why isn't the block editor doing link previews (basically rich link cards)?" Decisions (orchestrator recommendations, owner go 2026-10-05):

  1. Storage stays plain Markdown. A URL alone on its own line (a paragraph whose only content is one autolink/bare URL) renders as a card. A URL inside a sentence stays a normal link. Nothing new is written to the .md file.
  2. Default and override. Pasting a URL onto an empty line makes a card. ⌘Z / Ctrl+Z right after the paste turns it back into a plain link (keeps the URL inline, e.g. by inserting text after it is not required; use a per-block "Show as link" flag kept out of Markdown? NO: to stay plain Markdown, "as link" is represented as <URL> or [title](URL) which are inline links; a bare URL line = card). The block ⋯ menu offers "Show as card" / "Show as link", switching between those two Markdown forms.
  3. Card content: title, site name with favicon, a short description, og:image thumbnail on the right; compact one-line form at phone width; "domain · title" fallback when metadata is missing; loading state is the plain link (no skeleton), then the card fades in; errors fall back to the plain link silently.
  4. Fetching and privacy: the server fetches the page once through the existing outbound proxy used for mail images (round 9: no cookies, no referrer, private/internal address blocking, redirects re-checked, size/time bounded, content-type checked), parses only <head> metadata (og:, twitter:, title, icon) with a byte cap, caches the result as derived data in the index (TTL, revalidate in the background), never in the note. Images are served through the same proxy. Settings → Notes → "Show link previews" (on by default) turns fetching off; then lines stay plain links. The browser never contacts the site directly.
  5. Internal links: links to calternal items (notes, events, tasks, files, photos, deep links per DESIGN §33) render with the existing ItemPreview card, no fetch.
  6. Not now: playable embeds (video, maps, posts). File a follow-up only.

Gates: the fetcher gets the same adversarial probes as the mail proxy (tests/adversarial: SSRF to private ranges, DNS rebinding, redirect to localhost, huge pages, slow-loris, non-HTML, gzip bombs, Unicode/IDN hosts); cross-user isolation (cache keyed by URL, no user data in it). Screenshots of a note with external and internal cards at 390/820/1440, light + dark; e2e like a User: paste URL → card; ⌘Z → link; menu toggle; reload keeps it; offline shows the link.

## Owner request (2026-10-05): rich link cards in the block editor "Why isn't the block editor doing link previews (basically rich link cards)?" Decisions (orchestrator recommendations, owner go 2026-10-05): 1. **Storage stays plain Markdown.** A URL alone on its own line (a paragraph whose only content is one autolink/bare URL) renders as a card. A URL inside a sentence stays a normal link. Nothing new is written to the `.md` file. 2. **Default and override.** Pasting a URL onto an empty line makes a card. ⌘Z / Ctrl+Z right after the paste turns it back into a plain link (keeps the URL inline, e.g. by inserting text after it is not required; use a per-block "Show as link" flag kept out of Markdown? NO: to stay plain Markdown, "as link" is represented as `<URL>` or `[title](URL)` which are inline links; a bare URL line = card). The block ⋯ menu offers "Show as card" / "Show as link", switching between those two Markdown forms. 3. **Card content:** title, site name with favicon, a short description, og:image thumbnail on the right; compact one-line form at phone width; "domain · title" fallback when metadata is missing; loading state is the plain link (no skeleton), then the card fades in; errors fall back to the plain link silently. 4. **Fetching and privacy:** the server fetches the page once through the existing outbound proxy used for mail images (round 9: no cookies, no referrer, private/internal address blocking, redirects re-checked, size/time bounded, content-type checked), parses only `<head>` metadata (og:*, twitter:*, title, icon) with a byte cap, caches the result as derived data in the index (TTL, revalidate in the background), never in the note. Images are served through the same proxy. Settings → Notes → "Show link previews" (on by default) turns fetching off; then lines stay plain links. The browser never contacts the site directly. 5. **Internal links:** links to calternal items (notes, events, tasks, files, photos, deep links per DESIGN §33) render with the existing ItemPreview card, no fetch. 6. **Not now:** playable embeds (video, maps, posts). File a follow-up only. Gates: the fetcher gets the same adversarial probes as the mail proxy (tests/adversarial: SSRF to private ranges, DNS rebinding, redirect to localhost, huge pages, slow-loris, non-HTML, gzip bombs, Unicode/IDN hosts); cross-user isolation (cache keyed by URL, no user data in it). Screenshots of a note with external and internal cards at 390/820/1440, light + dark; e2e like a User: paste URL → card; ⌘Z → link; menu toggle; reload keeps it; offline shows the link.
Author
Owner

Starting #1151 on branch job/linkcards-1151, based on origin/dev at a493c314edfd40027a72e0d7fc1cb97fa69b1eb9 (checkout de654a42ac4f9a6dca83bed8e0eebc99706f7dfe). I am mapping the existing editor, proxy and Index code before adding the link-card behavior.

Starting #1151 on branch `job/linkcards-1151`, based on `origin/dev` at `a493c314edfd40027a72e0d7fc1cb97fa69b1eb9` (checkout `de654a42ac4f9a6dca83bed8e0eebc99706f7dfe`). I am mapping the existing editor, proxy and Index code before adding the link-card behavior.
Author
Owner

Finding for #1151: Notes already renders read-only Markdown link widgets from its shared Index in apps/web/src/lib/notes/inlineLinks.ts (#856), and external images already use calternal-plugin::raster_transport through /api/v1/images/proxy (#726/#766). I will extend those paths. The link preview cache will be derived data keyed only by the canonical URL; no User ID or Note content will enter it.

Finding for #1151: Notes already renders read-only Markdown link widgets from its shared Index in `apps/web/src/lib/notes/inlineLinks.ts` (#856), and external images already use `calternal-plugin::raster_transport` through `/api/v1/images/proxy` (#726/#766). I will extend those paths. The link preview cache will be derived data keyed only by the canonical URL; no User ID or Note content will enter it.
Author
Owner

Gate finding for #1151: bun run check stops in perf-lint --check before Svelte checking. The committed contracts/perf/exceptions.json has 21,977 entries while contracts/perf/ratchet.json records 21,964, and git show origin/dev:<file> reports the same counts. Neither file is changed in this worktree. This is a baseline mismatch on origin/dev, not an implementation finding; I will still run the focused TypeScript tests and Svelte check separately.

Gate finding for #1151: `bun run check` stops in `perf-lint --check` before Svelte checking. The committed `contracts/perf/exceptions.json` has 21,977 entries while `contracts/perf/ratchet.json` records 21,964, and `git show origin/dev:<file>` reports the same counts. Neither file is changed in this worktree. This is a baseline mismatch on `origin/dev`, not an implementation finding; I will still run the focused TypeScript tests and Svelte check separately.
Author
Owner

The live production-build E2E exposed a Rust/TypeScript Markdown bridge mismatch: after Cmd+Z, the browser submitted https://example.com/ instead of the requested angle autolink, so reload would not preserve the plain-link override. The Rust reader also left angle autolinks as text. Added regression vectors for standalone and in-prose HTTP angle autolinks; both now pass against the Rust bridge.

The live production-build E2E exposed a Rust/TypeScript Markdown bridge mismatch: after Cmd+Z, the browser submitted [https://example.com/](<https://example.com/>) instead of the requested angle autolink, so reload would not preserve the plain-link override. The Rust reader also left angle autolinks as text. Added regression vectors for standalone and in-prose HTTP angle autolinks; both now pass against the Rust bridge.
Author
Owner

The production E2E shows the pasted URL and its inline link in the local editor, and sends URL-bearing Yjs update frames, but the Notes API still returns the initial # Paste a URL after a 15-second save wait. Aborting the browser's link-preview request did not change the result, so the preview enqueue is not the cause. A later diagnostic run accepted the ordinary collaboration control edit, but the host stalled during a SQLite WAL checkpoint (28.8 seconds) and connection acquisition (25.6 seconds) before reaching the URL step. I am separating a rejected/delayed collaboration update from local host pressure with content-free server traces.

The production E2E shows the pasted URL and its inline link in the local editor, and sends URL-bearing Yjs update frames, but the Notes API still returns the initial `# Paste a URL` after a 15-second save wait. Aborting the browser's link-preview request did not change the result, so the preview enqueue is not the cause. A later diagnostic run accepted the ordinary collaboration control edit, but the host stalled during a SQLite WAL checkpoint (28.8 seconds) and connection acquisition (25.6 seconds) before reaching the URL step. I am separating a rejected/delayed collaboration update from local host pressure with content-free server traces.
Author
Owner

Continuing #1151 on job/linkcards-1151; saved head 848d2ab5c4, base de654a42ac. Preserving the prior six commits and remaining editor/Settings changes. Removing temporary flush diagnostics after reviewing their diff. Verifying internal ItemCard rendering, Markdown overrides, preference, production screenshots and per-crate gates. No push.

Continuing #1151 on job/linkcards-1151; saved head 848d2ab5c4f7569ab0b7f29b59ef9ce8b6e935e4, base de654a42ac4f9a6dca83bed8e0eebc99706f7dfe. Preserving the prior six commits and remaining editor/Settings changes. Removing temporary flush diagnostics after reviewing their diff. Verifying internal ItemCard rendering, Markdown overrides, preference, production screenshots and per-crate gates. No push.
Author
Owner

After resuming: fixed a cached-widget identity bug with a focused regression (disabling previews now replaces already-rendered cards). The focused Notes/block-menu tests pass: 2 files, 15 tests. Editor paste, collaboration Undo and Markdown suites pass: 3 files, 136 tests. Merged origin/dev 41bf97fb2 in b21994214, retaining both card lifecycle imports and pushState. Migration 0034 remains free. The preview lookup now rejects private IP literals before it queues a job; named hosts remain checked by the shared pinned transport. Added queue-exclusion and transport deadline regressions. Final gates are running; exact perf pins and shared shape tokens need refresh for the changed surface.

After resuming: fixed a cached-widget identity bug with a focused regression (disabling previews now replaces already-rendered cards). The focused Notes/block-menu tests pass: 2 files, 15 tests. Editor paste, collaboration Undo and Markdown suites pass: 3 files, 136 tests. Merged origin/dev 41bf97fb2 in b21994214, retaining both card lifecycle imports and pushState. Migration 0034 remains free. The preview lookup now rejects private IP literals before it queues a job; named hosts remain checked by the shared pinned transport. Added queue-exclusion and transport deadline regressions. Final gates are running; exact perf pins and shared shape tokens need refresh for the changed surface.
Author
Owner

UX gaps closed after the first browser journey: URL cards now render while the editor stays focused, rather than waiting for blur or an Index refresh; edits remove the old URL widgets and cancel obsolete lookup work. Copy link has a destination action in the shared block menu. Copy link to block preserves the card even after assigning its stable Block ID; card/link toggles keep that ID. Added regressions pass together with the unchanged Calendar Settings assertions: 3 files, 20 tests (30 s test timeout on the shared host). Notes and shared transport clippy have passed; Rust tests are compiling. Playable embeds remain excluded and have follow-up #1167.

UX gaps closed after the first browser journey: URL cards now render while the editor stays focused, rather than waiting for blur or an Index refresh; edits remove the old URL widgets and cancel obsolete lookup work. Copy link has a destination action in the shared block menu. Copy link to block preserves the card even after assigning its stable Block ID; card/link toggles keep that ID. Added regressions pass together with the unchanged Calendar Settings assertions: 3 files, 20 tests (30 s test timeout on the shared host). Notes and shared transport clippy have passed; Rust tests are compiling. Playable embeds remain excluded and have follow-up #1167.
Author
Owner

Completed remaining editor and Settings slices; current head bc715b818.

Evidence and fixes:

  • A pasted URL previously waited for editor blur or an Index refresh. Cards now rebuild on document changes while focused. The focused regression suite passes (20 tests).
  • A stable Block ID added by Copy link to block stopped URL card detection. Detection now strips only the recognized storage token; the new test keeps that same ID through card/link changes.
  • The shared block menu now copies the destination URL and keeps its separate Copy link to block action.
  • Settings uses the shared Toggle, persists the per-User preference, and offers Undo. The production journey checks Space and Undo.
  • Private IPv4/IPv6 literals are refused before enqueue. The Notes router regression also asserts that no fetch job is created. The shared transport deadline test confirms both permits are released.
  • The full web suite found numeric font weights in the new card CSS; fixed with shared weight tokens. Its guard passes. The unchanged Calendar Settings test exceeded 5 seconds on this host; its assertions pass with a longer test timeout. Full web tests are running with 30-second test/hook timeouts.

Transport gate output:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 58s
test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 22.68s

Current production assets have built. Capturing the final Notes and Settings screenshots at all six width/theme combinations with macOS platform emulation. Playable embeds remain the separate follow-up #1167. The full adversarial matrix and performance measurements remain for the merge round under the current verification policy.

Completed remaining editor and Settings slices; current head bc715b818. Evidence and fixes: - A pasted URL previously waited for editor blur or an Index refresh. Cards now rebuild on document changes while focused. The focused regression suite passes (20 tests). - A stable Block ID added by Copy link to block stopped URL card detection. Detection now strips only the recognized storage token; the new test keeps that same ID through card/link changes. - The shared block menu now copies the destination URL and keeps its separate Copy link to block action. - Settings uses the shared Toggle, persists the per-User preference, and offers Undo. The production journey checks Space and Undo. - Private IPv4/IPv6 literals are refused before enqueue. The Notes router regression also asserts that no fetch job is created. The shared transport deadline test confirms both permits are released. - The full web suite found numeric font weights in the new card CSS; fixed with shared weight tokens. Its guard passes. The unchanged Calendar Settings test exceeded 5 seconds on this host; its assertions pass with a longer test timeout. Full web tests are running with 30-second test/hook timeouts. Transport gate output: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 58s test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 22.68s ``` Current production assets have built. Capturing the final Notes and Settings screenshots at all six width/theme combinations with macOS platform emulation. Playable embeds remain the separate follow-up #1167. The full adversarial matrix and performance measurements remain for the merge round under the current verification policy.
Author
Owner

Production UI evidence is attached for #1151. Head: 7de35019f.

The focused journey passed with current production web assets and the saved feature backend from the previous run. The current Rust source has its own per-crate gates. All screenshots use macOS platform emulation; no review images are committed.

PASS paste → Cmd+Z → block-menu card/link + Copy link → reload → pointer/keyboard/touch card navigation → keyboard Settings + Undo → offline link

The phone touch check opens the real internal Note card and checks its height is at least 44 px. Keyboard Enter opens that card. Space changes the Notes setting; its Undo action restores the server value. Card/link overrides persist through reload. An unavailable metadata request leaves a plain URL link.

Screenshots for Claude review:

Decisions:

  • Reuse the shared ItemCard surface for internal URL cards. ItemPreview accepts only timed Calendar items and decks; Note/file/photo links cannot provide those payloads. Note and Task titles use the loaded Notes Index. Other route families use existing indexed link metadata or a kind label. No external metadata request is sent for these cards.
  • Keep stable Block IDs when changing presentation. A URL with a Block ID uses the existing writer's labelled Markdown link form for the plain-link override; it remains ordinary Markdown.
  • Playable embeds are not included; follow-up #1167 tracks them.

The full web suite was terminated by SIGTERM (exit 143) before a result. It is restarted with all tests, two workers, 30-second test/hook timeouts and the built-in transform cache. No assertions changed. The adversarial matrix and performance measurements remain for the merge round under the latest verification policy.

Production UI evidence is attached for #1151. Head: 7de35019f. The focused journey passed with current production web assets and the saved feature backend from the previous run. The current Rust source has its own per-crate gates. All screenshots use macOS platform emulation; no review images are committed. ``` PASS paste → Cmd+Z → block-menu card/link + Copy link → reload → pointer/keyboard/touch card navigation → keyboard Settings + Undo → offline link ``` The phone touch check opens the real internal Note card and checks its height is at least 44 px. Keyboard Enter opens that card. Space changes the Notes setting; its Undo action restores the server value. Card/link overrides persist through reload. An unavailable metadata request leaves a plain URL link. Screenshots for Claude review: - [link-cards-1440-dark.png](https://git.kayg.org/attachments/5ec8d2a6-ff57-4763-aeb7-76121ee200d6) - [link-cards-1440-light.png](https://git.kayg.org/attachments/ab4cae51-3b94-46e9-ad3d-e02d2f71fe3d) - [link-cards-390-dark.png](https://git.kayg.org/attachments/32780f86-c377-4266-8d35-da3ffb1f5f41) - [link-cards-390-light.png](https://git.kayg.org/attachments/00f6075b-f35f-48b4-8db5-dbfe0a8ebad7) - [link-cards-820-dark.png](https://git.kayg.org/attachments/aec7532b-2e84-4790-9f1d-3553e675c4d7) - [link-cards-820-light.png](https://git.kayg.org/attachments/eaca6601-3a15-4f26-a2c7-e49cbddfe447) - [link-preview-settings-1440-dark.png](https://git.kayg.org/attachments/e1defc4f-41a2-4f7b-bfcc-02ee2d269e1e) - [link-preview-settings-1440-light.png](https://git.kayg.org/attachments/04c85fc1-3176-447b-8c00-e6a755f65725) - [link-preview-settings-390-dark.png](https://git.kayg.org/attachments/4b567e30-ef6a-4e52-adb8-def37ce59036) - [link-preview-settings-390-light.png](https://git.kayg.org/attachments/25a137be-8d70-405f-b222-cf54ee9253b5) - [link-preview-settings-820-dark.png](https://git.kayg.org/attachments/638e5762-711d-4b5d-a788-bc6db18cab89) - [link-preview-settings-820-light.png](https://git.kayg.org/attachments/6923d2b7-8cec-42f6-9238-e7cbfa226024) Decisions: - Reuse the shared ItemCard surface for internal URL cards. ItemPreview accepts only timed Calendar items and decks; Note/file/photo links cannot provide those payloads. Note and Task titles use the loaded Notes Index. Other route families use existing indexed link metadata or a kind label. No external metadata request is sent for these cards. - Keep stable Block IDs when changing presentation. A URL with a Block ID uses the existing writer's labelled Markdown link form for the plain-link override; it remains ordinary Markdown. - Playable embeds are not included; follow-up #1167 tracks them. The full web suite was terminated by SIGTERM (exit 143) before a result. It is restarted with all tests, two workers, 30-second test/hook timeouts and the built-in transform cache. No assertions changed. The adversarial matrix and performance measurements remain for the merge round under the latest verification policy.
Author
Owner

Head dab7cc22a adds controlled upstream probes to tests/adversarial. The first extension covered rejected URL forms but left the response matrix incomplete.

The image-proxy section now reuses the Calendar fixture and the test-only DNS/socket shim. Notes keeps its production HTTP port policy. The shim maps only synthetic test addresses to the local high-port fixture, with a separate Notes rebinding counter. The matrix checks private redirects, redirect loops, oversized HTML, compressed responses, non-HTML, slow responses, IDN hosts, DNS pinning, identity-header absence, the shared public cache, per-User setting isolation and a denied burst. The new fixture tests read only fixture declarations and cannot load the probe's User session files.

Focused output:

Ran 2 tests in 1.292s

OK

Python compilation, C compilation with -Wall -Wextra -Werror, and bash -n pass. The real-server matrix must run in the merge round:

ROUND2_SECTIONS=image-proxy tests/adversarial/run.sh

Completed web and Notes gates:

svelte-check found 0 errors and 4 warnings in 3 files
 Test Files  266 passed (266)
      Tests  1838 passed (1838)
test result: ok. 293 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 469.03s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.41s

The full web run used two workers, 30-second test/hook timeouts and Vitest's transform cache. Assertions and file isolation remain unchanged. Collaborative Markdown and server gates are still running. No push.

Head dab7cc22a adds controlled upstream probes to tests/adversarial. The first extension covered rejected URL forms but left the response matrix incomplete. The image-proxy section now reuses the Calendar fixture and the test-only DNS/socket shim. Notes keeps its production HTTP port policy. The shim maps only synthetic test addresses to the local high-port fixture, with a separate Notes rebinding counter. The matrix checks private redirects, redirect loops, oversized HTML, compressed responses, non-HTML, slow responses, IDN hosts, DNS pinning, identity-header absence, the shared public cache, per-User setting isolation and a denied burst. The new fixture tests read only fixture declarations and cannot load the probe's User session files. Focused output: ``` Ran 2 tests in 1.292s OK ``` Python compilation, C compilation with -Wall -Wextra -Werror, and bash -n pass. The real-server matrix must run in the merge round: ``` ROUND2_SECTIONS=image-proxy tests/adversarial/run.sh ``` Completed web and Notes gates: ``` svelte-check found 0 errors and 4 warnings in 3 files Test Files 266 passed (266) Tests 1838 passed (1838) test result: ok. 293 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 469.03s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.41s ``` The full web run used two workers, 30-second test/hook timeouts and Vitest's transform cache. Assertions and file isolation remain unchanged. Collaborative Markdown and server gates are still running. No push.
Author
Owner

Production browser verification passed, including live internal Note rename, pointer/keyboard/touch navigation, paste Undo, persisted card/link overrides, Copy link, Settings Retry/Undo and offline fallback. The final 18 macOS-emulated screenshots are attached for 390, 820 and 1440 px in light/dark, including the Settings read-error state.

Server gates found the expected new Notes 0034 receipt missing from three upgrade assertions: production 7b pending receipts are 17 instead of 16; round 9 includes Notes 34; the 7c upgrade has four additions instead of three. The test changes explicitly include 0034 and leave all pinned deployed SQL and original-receipt checks intact. This is an expectation change caused by the new migration in #1151, not a weakened check.

The first server run also hit the unchanged Calendar startup test’s 20-second request timeout under host load. No timeout expectation was changed. After the migration assertions, server fmt/clippy/test is running again with four test threads. Full web check and test pass (267 files, 1843 tests).

Production browser verification passed, including live internal Note rename, pointer/keyboard/touch navigation, paste Undo, persisted card/link overrides, Copy link, Settings Retry/Undo and offline fallback. The final 18 macOS-emulated screenshots are attached for 390, 820 and 1440 px in light/dark, including the Settings read-error state. Server gates found the expected new Notes 0034 receipt missing from three upgrade assertions: production 7b pending receipts are 17 instead of 16; round 9 includes Notes 34; the 7c upgrade has four additions instead of three. The test changes explicitly include 0034 and leave all pinned deployed SQL and original-receipt checks intact. This is an expectation change caused by the new migration in #1151, not a weakened check. The first server run also hit the unchanged Calendar startup test’s 20-second request timeout under host load. No timeout expectation was changed. After the migration assertions, server fmt/clippy/test is running again with four test threads. Full web check and test pass (267 files, 1843 tests).
Author
Owner

Built #1151: standalone Markdown URL cards, immediate paste Undo, persisted card/link actions, internal ItemCard links, and the per-User Notes setting with Undo and Retry.

Head: 7ac4aa7f7872e7e4d96114e72ada136a43698a4c on job/linkcards-1151. Merged the fetched origin/dev snapshot 41bf97fb2 once. No push, deploy or merge into dev. Working tree is clean; this job’s Cargo target and web build output were removed after verification. Migration 0034 remains free on the newer remote snapshot checked during validation.

UX gaps closed:

  • Cards now appear while editing, before blur or Index refresh.
  • Stable Block IDs no longer prevent a card; presentation changes keep the ID.
  • The shared block menu copies the destination separately from the Block link.
  • Internal Note cards open by pointer, keyboard Enter and phone touch. Their titles update after a real Note rename without a reload; unchanged titles keep the same widget DOM.
  • Settings waits for a confirmed value. Failed writes keep that value. Undo restores it. Retry uses the shared Pill with its 44 px hit floor.
  • Pending and failed metadata reads stay plain links; reconnect can retry. The saved preference stops editor lookups and server fetch queueing.
  • URL work is bounded. Removed URLs do not keep old requests queued.

Security and probes:

  • Public metadata uses the shared credential-free transport and Image proxy. Known private literals are refused before queueing.
  • The URL-only Index cache and queue payload have no User or Note field. Settings remain per User.
  • The controlled probe matrix covers URL admission, private redirects, redirect loops, HTML byte limits, encoding, non-HTML, slow response deadlines, IDN, DNS pinning, request identity headers, public cache sharing, setting isolation and denied bursts.

Decisions:

  • Ready metadata lasts seven days; failed fetches last 15 minutes. The shared cache holds at most 10,000 URLs.
  • One editor admits 32 standalone URLs, keeps 64 cache entries, runs four lookup chains and polls each at most eight times, 1.5 seconds apart.
  • Internal links reuse ItemCard. ItemPreview requires timed Calendar/deck payloads that generic Note/file/photo URLs do not have. Titles use indexed data when available; other stable routes use a kind label. They do not use external metadata fetching.
  • A plain-link URL with a Block ID uses the existing Markdown writer's labelled link syntax. The storage token remains unchanged.
  • The fetcher probe reuses the existing local Calendar fixture and test-only DNS shim. It keeps production ports unchanged and uses a separate Notes lookup counter.

Known gaps / UX gaps left:

  • Server test is not fully green: 255 passed, one wrapper failed, ten ignored. Its child hit the unchanged 15-second HTTP-startup timeout tracked by #1161 and #1169. All four migration upgrade checks pass. This is the only final Rust gate failure; no timeout or assertion was weakened.
  • Playable embeds remain the owner-approved follow-up #1167.
  • Internal routes without indexed metadata show a kind label; this feature does not load full Calendar item details into the card.
  • Claude must review the attached production screenshots. Mac platform emulation is included; real Mac interop is for the merge round.
  • Performance was not measured: this is a feature issue, and the current policy runs measurements only for performance issues. bench/link-previews-1151.mjs adds warm lookup p50/p95, CPU/RSS and a 32-URL/64-request cold burst profile. No baseline comparison is claimed.

For the merge round:

ROUND2_SECTIONS=image-proxy tests/adversarial/run.sh
CALTERNAL_SERVER_BIN=<combined-server> CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/linkcards-1151.mjs

The first command must show that the real worker refuses hostile upstream responses and cannot reach the private trap, with no crash, 5xx or User isolation failure. The second must prove the same production journey against the combined server build. Re-run cargo test -p calternal-server -- --test-threads=4 after the #1161 startup fix. It must pass with the original startup deadlines. Run the full XUser/authz matrices, staging gates and real Mac checks in the merge round under its existing commands. Run the profile only when the performance policy schedules it, holding /root/perf.lock on the perf VM and recording its load average.

Gate output (verbatim; server test exits 101 on the known startup issue):

cargo fmt --check: exit 0, no output.

cargo clippy -p calternal-plugin --all-targets -- -D warnings and cargo test -p calternal-plugin:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 58s
test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 22.68s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings and cargo test -p calternal-plugin-notes:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 21m 14s
test result: ok. 293 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 469.03s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.41s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-collab --all-targets -- -D warnings and cargo test -p calternal-collab:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5m 29s
test result: ok. 92 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 67.39s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.66s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.57s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.10s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 155.57s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.21s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.36s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 12.40s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.51s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.78s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.27s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.64s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 78.39s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.63s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 51.81s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-server --all-targets -- -D warnings and cargo test -p calternal-server -- --test-threads=4:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.88s
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 265 filtered out; finished in 19.88s
test result: FAILED. 255 passed; 1 failed; 10 ignored; 0 measured; 0 filtered out; finished in 97.83s

cd apps/web && bun run check:

perf-lint: PASS; 0 violations; 22109 scoped exceptions
svelte-check found 0 errors and 4 warnings in 3 files

cd apps/web && bun run test --maxWorkers=2 --testTimeout=30000 --hookTimeout=30000 --fsModuleCache (exit 0):

 Test Files  267 passed (267)
      Tests  1843 passed (1843)

Focused Notes/Settings/block-menu tests:

 Test Files  3 passed (3)
      Tests  22 passed (22)

Focused editor tests:

 Test Files  3 passed (3)
      Tests  136 passed (136)

Controlled fetch fixture and DNS/socket shim tests:

..
----------------------------------------------------------------------
Ran 2 tests in 1.292s

OK

Production browser journey:

PASS paste → Cmd+Z → block-menu card/link + Copy link → reload → pointer/keyboard/touch card navigation + live rename → keyboard Settings + Retry + Undo → offline link

Existing test expectations changed only where #1151 adds migration 0034: the Notes migration list and the server production-upgrade receipt lists/counts. Pinned deployed SQL, checksum checks and preserved receipts remain unchanged.

Screenshots: 18 production screenshots are attached below. They cover Notes cards, normal Settings, and Settings read-error/Retry at 390/820/1440 px, light/dark, with macOS platform emulation. The journey used the previous feature server binary and the final production web assets; the merge round must repeat it against its combined backend.

Files (43 changed files relative to the merged snapshot):

  • Cargo.lock
  • apps/web/e2e/linkcards-1151.mjs
  • apps/web/src/lib/editor/blockMenu.test.ts
  • apps/web/src/lib/editor/blockMenu.ts
  • apps/web/src/lib/notes/NoteEditorSurface.svelte
  • apps/web/src/lib/notes/NoteView.svelte
  • apps/web/src/lib/notes/api.ts
  • apps/web/src/lib/notes/editor-types/index.d.ts
  • apps/web/src/lib/notes/editorHost.ts
  • apps/web/src/lib/notes/inlineLinks.test.ts
  • apps/web/src/lib/notes/inlineLinks.ts
  • apps/web/src/routes/settings/[...path]/+page.svelte
  • apps/web/src/routes/settings/notes/NotesSection.svelte
  • apps/web/src/routes/settings/notes/NotesSection.svelte.test.ts
  • apps/web/src/routes/settings/sections.ts
  • bench/link-previews-1151.mjs
  • contracts/action-policy.json
  • contracts/openapi.json
  • contracts/perf/adoption-1058.json
  • contracts/perf/exceptions.json
  • contracts/perf/ratchet.json
  • crates/calternal-server/src/upgrade_tests.rs
  • crates/calternal-collab/Cargo.toml
  • crates/calternal-collab/src/markdown.rs
  • crates/calternal-plugin/src/outbound.rs
  • crates/calternal-plugin/src/raster_transport.rs
  • crates/plugins/notes/migrations/0034_link_previews.sql
  • crates/plugins/notes/src/lib.rs
  • crates/plugins/notes/src/link_previews.rs
  • crates/plugins/notes/src/reminders_tests.rs
  • crates/plugins/notes/src/store.rs
  • packages/api-client/src/generated.ts
  • packages/editor/src/Editor.svelte
  • packages/editor/src/bareUrlPaste.test.ts
  • packages/editor/src/collaborationUndo.test.ts
  • packages/editor/src/extensions.ts
  • packages/editor/src/markdown.test.ts
  • packages/editor/src/markdown.ts
  • packages/editor/src/source.ts
  • tests/adversarial/attack2.py
  • tests/adversarial/dns_rebind_preload.c
  • tests/adversarial/run.sh
  • tests/adversarial/test_link_preview_fixtures.py

Screenshot attachments:

Validation history: initial server runs found outdated migration counts, now fixed. They also hit unchanged Calendar request (20 s) and HTTP startup (15 s) timeouts under shared-host load. No timeout or behavior assertion was weakened. The final result above is from the corrected source. A tool-attached gate runner was interrupted during compilation; the final runner saved its actual exit status. Web validation used 30 s test/hook timeouts for this shared host.

Isolated confirmation of #1161 / #1169 (no timeout change):
RUST_MIN_STACK=8388608 cargo test -p calternal-server wire::tests::startup_serves_http_while_upgrade_backfills_wait -- --exact --ignored --test-threads=1

HTTP startup waited for an upgrade backfill: Elapsed(())
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 265 filtered out; finished in 22.12s

Cleanup: cargo clean reported Removed 23051 files, 21.6GiB total. Removed apps/web/build and apps/web/.svelte-kit/output. Review artifacts remain ignored in this worktree.

Built #1151: standalone Markdown URL cards, immediate paste Undo, persisted card/link actions, internal ItemCard links, and the per-User Notes setting with Undo and Retry. Head: `7ac4aa7f7872e7e4d96114e72ada136a43698a4c` on `job/linkcards-1151`. Merged the fetched `origin/dev` snapshot `41bf97fb2` once. No push, deploy or merge into dev. Working tree is clean; this job’s Cargo target and web build output were removed after verification. Migration 0034 remains free on the newer remote snapshot checked during validation. UX gaps closed: - Cards now appear while editing, before blur or Index refresh. - Stable Block IDs no longer prevent a card; presentation changes keep the ID. - The shared block menu copies the destination separately from the Block link. - Internal Note cards open by pointer, keyboard Enter and phone touch. Their titles update after a real Note rename without a reload; unchanged titles keep the same widget DOM. - Settings waits for a confirmed value. Failed writes keep that value. Undo restores it. Retry uses the shared Pill with its 44 px hit floor. - Pending and failed metadata reads stay plain links; reconnect can retry. The saved preference stops editor lookups and server fetch queueing. - URL work is bounded. Removed URLs do not keep old requests queued. Security and probes: - Public metadata uses the shared credential-free transport and Image proxy. Known private literals are refused before queueing. - The URL-only Index cache and queue payload have no User or Note field. Settings remain per User. - The controlled probe matrix covers URL admission, private redirects, redirect loops, HTML byte limits, encoding, non-HTML, slow response deadlines, IDN, DNS pinning, request identity headers, public cache sharing, setting isolation and denied bursts. Decisions: - Ready metadata lasts seven days; failed fetches last 15 minutes. The shared cache holds at most 10,000 URLs. - One editor admits 32 standalone URLs, keeps 64 cache entries, runs four lookup chains and polls each at most eight times, 1.5 seconds apart. - Internal links reuse ItemCard. ItemPreview requires timed Calendar/deck payloads that generic Note/file/photo URLs do not have. Titles use indexed data when available; other stable routes use a kind label. They do not use external metadata fetching. - A plain-link URL with a Block ID uses the existing Markdown writer's labelled link syntax. The storage token remains unchanged. - The fetcher probe reuses the existing local Calendar fixture and test-only DNS shim. It keeps production ports unchanged and uses a separate Notes lookup counter. Known gaps / UX gaps left: - Server test is not fully green: 255 passed, one wrapper failed, ten ignored. Its child hit the unchanged 15-second HTTP-startup timeout tracked by #1161 and #1169. All four migration upgrade checks pass. This is the only final Rust gate failure; no timeout or assertion was weakened. - Playable embeds remain the owner-approved follow-up #1167. - Internal routes without indexed metadata show a kind label; this feature does not load full Calendar item details into the card. - Claude must review the attached production screenshots. Mac platform emulation is included; real Mac interop is for the merge round. - Performance was not measured: this is a feature issue, and the current policy runs measurements only for performance issues. `bench/link-previews-1151.mjs` adds warm lookup p50/p95, CPU/RSS and a 32-URL/64-request cold burst profile. No baseline comparison is claimed. For the merge round: ``` ROUND2_SECTIONS=image-proxy tests/adversarial/run.sh CALTERNAL_SERVER_BIN=<combined-server> CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/linkcards-1151.mjs ``` The first command must show that the real worker refuses hostile upstream responses and cannot reach the private trap, with no crash, 5xx or User isolation failure. The second must prove the same production journey against the combined server build. Re-run `cargo test -p calternal-server -- --test-threads=4` after the #1161 startup fix. It must pass with the original startup deadlines. Run the full XUser/authz matrices, staging gates and real Mac checks in the merge round under its existing commands. Run the profile only when the performance policy schedules it, holding `/root/perf.lock` on the perf VM and recording its load average. Gate output (verbatim; server test exits 101 on the known startup issue): `cargo fmt --check`: exit 0, no output. `cargo clippy -p calternal-plugin --all-targets -- -D warnings` and `cargo test -p calternal-plugin`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 58s test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 22.68s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings` and `cargo test -p calternal-plugin-notes`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 21m 14s test result: ok. 293 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 469.03s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.41s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-collab --all-targets -- -D warnings` and `cargo test -p calternal-collab`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 5m 29s test result: ok. 92 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 67.39s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.66s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.57s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.10s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 155.57s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.21s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.36s test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 12.40s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.51s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.78s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.27s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.64s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 78.39s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.63s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 51.81s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-server --all-targets -- -D warnings` and `cargo test -p calternal-server -- --test-threads=4`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.88s test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 265 filtered out; finished in 19.88s test result: FAILED. 255 passed; 1 failed; 10 ignored; 0 measured; 0 filtered out; finished in 97.83s ``` `cd apps/web && bun run check`: ``` perf-lint: PASS; 0 violations; 22109 scoped exceptions svelte-check found 0 errors and 4 warnings in 3 files ``` `cd apps/web && bun run test --maxWorkers=2 --testTimeout=30000 --hookTimeout=30000 --fsModuleCache` (exit 0): ``` Test Files 267 passed (267) Tests 1843 passed (1843) ``` Focused Notes/Settings/block-menu tests: ``` Test Files 3 passed (3) Tests 22 passed (22) ``` Focused editor tests: ``` Test Files 3 passed (3) Tests 136 passed (136) ``` Controlled fetch fixture and DNS/socket shim tests: ``` .. ---------------------------------------------------------------------- Ran 2 tests in 1.292s OK ``` Production browser journey: ``` PASS paste → Cmd+Z → block-menu card/link + Copy link → reload → pointer/keyboard/touch card navigation + live rename → keyboard Settings + Retry + Undo → offline link ``` Existing test expectations changed only where #1151 adds migration 0034: the Notes migration list and the server production-upgrade receipt lists/counts. Pinned deployed SQL, checksum checks and preserved receipts remain unchanged. Screenshots: 18 production screenshots are attached below. They cover Notes cards, normal Settings, and Settings read-error/Retry at 390/820/1440 px, light/dark, with macOS platform emulation. The journey used the previous feature server binary and the final production web assets; the merge round must repeat it against its combined backend. Files (43 changed files relative to the merged snapshot): - `Cargo.lock` - `apps/web/e2e/linkcards-1151.mjs` - `apps/web/src/lib/editor/blockMenu.test.ts` - `apps/web/src/lib/editor/blockMenu.ts` - `apps/web/src/lib/notes/NoteEditorSurface.svelte` - `apps/web/src/lib/notes/NoteView.svelte` - `apps/web/src/lib/notes/api.ts` - `apps/web/src/lib/notes/editor-types/index.d.ts` - `apps/web/src/lib/notes/editorHost.ts` - `apps/web/src/lib/notes/inlineLinks.test.ts` - `apps/web/src/lib/notes/inlineLinks.ts` - `apps/web/src/routes/settings/[...path]/+page.svelte` - `apps/web/src/routes/settings/notes/NotesSection.svelte` - `apps/web/src/routes/settings/notes/NotesSection.svelte.test.ts` - `apps/web/src/routes/settings/sections.ts` - `bench/link-previews-1151.mjs` - `contracts/action-policy.json` - `contracts/openapi.json` - `contracts/perf/adoption-1058.json` - `contracts/perf/exceptions.json` - `contracts/perf/ratchet.json` - `crates/calternal-server/src/upgrade_tests.rs` - `crates/calternal-collab/Cargo.toml` - `crates/calternal-collab/src/markdown.rs` - `crates/calternal-plugin/src/outbound.rs` - `crates/calternal-plugin/src/raster_transport.rs` - `crates/plugins/notes/migrations/0034_link_previews.sql` - `crates/plugins/notes/src/lib.rs` - `crates/plugins/notes/src/link_previews.rs` - `crates/plugins/notes/src/reminders_tests.rs` - `crates/plugins/notes/src/store.rs` - `packages/api-client/src/generated.ts` - `packages/editor/src/Editor.svelte` - `packages/editor/src/bareUrlPaste.test.ts` - `packages/editor/src/collaborationUndo.test.ts` - `packages/editor/src/extensions.ts` - `packages/editor/src/markdown.test.ts` - `packages/editor/src/markdown.ts` - `packages/editor/src/source.ts` - `tests/adversarial/attack2.py` - `tests/adversarial/dns_rebind_preload.c` - `tests/adversarial/run.sh` - `tests/adversarial/test_link_preview_fixtures.py` Screenshot attachments: - [link-cards-1440-dark.png](https://git.kayg.org/attachments/da0bc36f-956f-4bcd-a1a4-40ea852d8d41) - [link-cards-1440-light.png](https://git.kayg.org/attachments/51fca280-4ea8-4bd5-95b7-d4ba0961c561) - [link-cards-390-dark.png](https://git.kayg.org/attachments/450bbe48-9d2b-4fb8-b87a-d7525fdaee76) - [link-cards-390-light.png](https://git.kayg.org/attachments/3144a2b9-ff3c-4b7d-a024-7a68e521f9cc) - [link-cards-820-dark.png](https://git.kayg.org/attachments/56086610-835d-4cde-af80-86f71280425a) - [link-cards-820-light.png](https://git.kayg.org/attachments/46c79628-eb32-4640-bbef-afc2aabc66fb) - [link-preview-settings-1440-dark.png](https://git.kayg.org/attachments/c35284e3-c86b-4364-acf7-e53c48054585) - [link-preview-settings-1440-light.png](https://git.kayg.org/attachments/f4075478-378a-45fd-a775-736df47489d4) - [link-preview-settings-390-dark.png](https://git.kayg.org/attachments/2dd06173-fc77-4b80-a174-9019a2d0b6b5) - [link-preview-settings-390-light.png](https://git.kayg.org/attachments/79d06e6f-9068-4a9c-bd3a-0d51bb9cc51e) - [link-preview-settings-820-dark.png](https://git.kayg.org/attachments/ded11891-b06e-40e7-a8e3-1e2adde6c1c3) - [link-preview-settings-820-light.png](https://git.kayg.org/attachments/6eac8645-3456-40fb-baca-4b0a89199cf9) - [link-preview-settings-error-1440-dark.png](https://git.kayg.org/attachments/7d1274c5-a994-4428-88cb-570e47ce6849) - [link-preview-settings-error-1440-light.png](https://git.kayg.org/attachments/9fe9f152-7729-48ab-8bad-a5703cef6f42) - [link-preview-settings-error-390-dark.png](https://git.kayg.org/attachments/acada7cf-b414-49ca-8fcb-2c1795a9cde0) - [link-preview-settings-error-390-light.png](https://git.kayg.org/attachments/0f32ece0-fc8f-4bb5-b2a1-1e569ea90496) - [link-preview-settings-error-820-dark.png](https://git.kayg.org/attachments/769a4fe1-dca5-40ce-b655-5dbac90ee8e6) - [link-preview-settings-error-820-light.png](https://git.kayg.org/attachments/485e1b7c-8824-48fb-b084-7bf196875300) Validation history: initial server runs found outdated migration counts, now fixed. They also hit unchanged Calendar request (20 s) and HTTP startup (15 s) timeouts under shared-host load. No timeout or behavior assertion was weakened. The final result above is from the corrected source. A tool-attached gate runner was interrupted during compilation; the final runner saved its actual exit status. Web validation used 30 s test/hook timeouts for this shared host. Isolated confirmation of #1161 / #1169 (no timeout change): `RUST_MIN_STACK=8388608 cargo test -p calternal-server wire::tests::startup_serves_http_while_upgrade_backfills_wait -- --exact --ignored --test-threads=1` ``` HTTP startup waited for an upgrade backfill: Elapsed(()) test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 265 filtered out; finished in 22.12s ``` Cleanup: `cargo clean` reported `Removed 23051 files, 21.6GiB total`. Removed `apps/web/build` and `apps/web/.svelte-kit/output`. Review artifacts remain ignored in this worktree.
Author
Owner

Orchestrator review of artifacts/linkcards-1151/screenshots/link-cards-1440-light.png (head 7ac4aa7f7): the plumbing works, but the cards are not "rich link cards" yet — they look like flat grey text boxes with underlined links, the two cards are stuck together with no gap, and the external card shows no description, no favicon, no thumbnail.

Design (taste is the point here; study how good editors render link cards and make it calternal):

  1. External card: one rounded card (shared card/glass tokens, card radius, hairline border, subtle hover lift), content laid out as: left column = favicon (16 px) + site name in small muted text on line 1, the page title in body semibold on line 2 (max 2 lines, ellipsis), the description in small muted text (max 2 lines); right side = og:image thumbnail (aspect-fill, rounded, fixed width ~120 px at desktop) when present. No underlines; the whole card is the link (opens in a new tab with the shared external-link affordance), keyboard focusable with the shared focus ring. When metadata is missing: favicon + domain + title only, never an empty grey box. Use a real test page with og:title/og:description/og:image served by the local fixture server for screenshots (not example.com).
  2. Internal card (calternal item): reuse the ItemPreview visual language (kind icon, title, meta line e.g. "Note · Edited today"), same card frame as external, no "Link target" underline styling.
  3. Spacing: cards are blocks with the normal block gap between them and from surrounding text; width = text column (or a max width), aligned to the text's left edge (4x crop proof).
  4. Phone (390): compact one-line form (favicon + title, thumbnail hidden), still a card.
  5. Hover shows the block action pill per #1152 (Show as link / Copy link), not bespoke buttons.
    Merge origin/dev; bun run check + bun run test pass. Retake: external with full metadata, external with no image, internal note, internal event, phone, light + dark, plus 4x crops. Commit; comment on #1151 with head SHA. Do not push.
Orchestrator review of artifacts/linkcards-1151/screenshots/link-cards-1440-light.png (head 7ac4aa7f7): the plumbing works, but the cards are not "rich link cards" yet — they look like flat grey text boxes with underlined links, the two cards are stuck together with no gap, and the external card shows no description, no favicon, no thumbnail. Design (taste is the point here; study how good editors render link cards and make it calternal): 1. **External card:** one rounded card (shared card/glass tokens, card radius, hairline border, subtle hover lift), content laid out as: left column = favicon (16 px) + site name in small muted text on line 1, the page title in body semibold on line 2 (max 2 lines, ellipsis), the description in small muted text (max 2 lines); right side = og:image thumbnail (aspect-fill, rounded, fixed width ~120 px at desktop) when present. No underlines; the whole card is the link (opens in a new tab with the shared external-link affordance), keyboard focusable with the shared focus ring. When metadata is missing: favicon + domain + title only, never an empty grey box. Use a real test page with og:title/og:description/og:image served by the local fixture server for screenshots (not example.com). 2. **Internal card** (calternal item): reuse the ItemPreview visual language (kind icon, title, meta line e.g. "Note · Edited today"), same card frame as external, no "Link target" underline styling. 3. **Spacing:** cards are blocks with the normal block gap between them and from surrounding text; width = text column (or a max width), aligned to the text's left edge (4x crop proof). 4. Phone (390): compact one-line form (favicon + title, thumbnail hidden), still a card. 5. Hover shows the block action pill per #1152 (Show as link / Copy link), not bespoke buttons. Merge origin/dev; `bun run check` + `bun run test` pass. Retake: external with full metadata, external with no image, internal note, internal event, phone, light + dark, plus 4x crops. Commit; comment on #1151 with head SHA. Do not push.
Author
Owner

Started linkcards3-1151 on job/linkcards-1151, base 7ac4aa7f7872e7e4d96114e72ada136a43698a4c. I will revise the shared card framing, block spacing and compact phone presentation, and capture production screenshots with metadata fixtures. No new dependencies are planned.

Started linkcards3-1151 on `job/linkcards-1151`, base `7ac4aa7f7872e7e4d96114e72ada136a43698a4c`. I will revise the shared card framing, block spacing and compact phone presentation, and capture production screenshots with metadata fixtures. No new dependencies are planned.
Author
Owner

Finding: noteProse.css applies .editor-surface .cal-prose a { text-decoration: underline; } and resets paragraph margins after the block-rhythm rule. Those rules overrode the rich-card link decoration and removed its block gap. The card host now uses a specific card selector and restores --rhythm spacing on card paragraphs. Metadata screenshots now use the existing test DNS/socket shim with local HTML and PNG responses through the real metadata worker and image route. No production transport exception was added.

Decisions: reuse ItemCard passive content with a kind meta line for internal items; its host owns the same frame as external cards. Use a passive domain initial when a favicon is absent or fails. No new package or crate versions are needed.

Finding: `noteProse.css` applies `.editor-surface .cal-prose a { text-decoration: underline; }` and resets paragraph margins after the block-rhythm rule. Those rules overrode the rich-card link decoration and removed its block gap. The card host now uses a specific card selector and restores `--rhythm` spacing on card paragraphs. Metadata screenshots now use the existing test DNS/socket shim with local HTML and PNG responses through the real metadata worker and image route. No production transport exception was added. Decisions: reuse `ItemCard` passive content with a kind meta line for internal items; its host owns the same frame as external cards. Use a passive domain initial when a favicon is absent or fails. No new package or crate versions are needed.
Author
Owner

UX gap closed: a route-only Event had only the fallback title "Event" because it has no row in the Notes Index. The Note card host now subscribes to the existing shared card resolver for Events. It renders the returned title and subtitle through reactive ItemCard props and releases the subscription with the ProseMirror widget. The existing resolver batches requests and owns User invalidation and retry limits. The production journey now seeds a real CalDAV Event and requires its title before capture.

UX gap closed: a route-only Event had only the fallback title "Event" because it has no row in the Notes Index. The Note card host now subscribes to the existing shared card resolver for Events. It renders the returned title and subtitle through reactive `ItemCard` props and releases the subscription with the ProseMirror widget. The existing resolver batches requests and owns User invalidation and retry limits. The production journey now seeds a real CalDAV Event and requires its title before capture.
Author
Owner

Built the rich Note link cards requested in linkcards3-1151. Head: ee44d667245215886b9e2406e24c0c432393c3f9. Branch: job/linkcards-1151. Merged origin/dev once. No push, deploy or publication of code.

External cards use the shared card radius, glass tint and hairline frame. They have a site favicon, site name, semibold title, a two-line description and a 120 px raster thumbnail. The card is one focusable new-tab link. Phone cards use one line and hide the thumbnail. Cards keep the text column's left edge and normal block gap. Internal Note and Event cards reuse passive ItemCard content in the same frame. Events use the existing batched card resolver and the shared date/time formatter. Hover uses the existing block action pill.

Files: apps/web/src/lib/notes/NoteView.svelte, apps/web/src/lib/notes/inlineLinks.ts, apps/web/src/lib/notes/inlineLinks.test.ts, apps/web/e2e/linkcards-1151.mjs, contracts/perf/exceptions.json, contracts/perf/adoption-1058.json. Performance pins match the exact live source; rules and ratchets did not change. No dependencies changed.

Commits: 7fc0c290f (card frame, metadata and Event titles), fd112a562 (date/time formatting), ee44d6672 (real metadata fixtures and review evidence).

Gate output (verbatim summary)

cargo fmt --check: exit 0, no output. No Rust crate changed, so no Rust compilation, clippy or test run was needed.

cd apps/web && bun run check: exit 0.

perf-lint: PASS; 0 violations; 22109 scoped exceptions
svelte-check found 0 errors and 4 warnings in 3 files

cd apps/web && bun run test --maxWorkers=2: exit 0.

 Test Files  267 passed (267)
      Tests  1847 passed (1847)
   Start at  05:06:15
   Duration  1129.65s (transform 33%, environment 25%, import 20%, tests 16%, setup 6%)

Focused inlineLinks.test.ts: 13 passed. Final production build: exit 0. Production user journey: exit 0.

PASS paste → Cmd+Z → block-menu card/link + Copy link → reload → pointer/keyboard/touch card navigation + live rename → keyboard Settings + Retry + Undo → offline link
PASS metadata + no-image + Note + Event cards at every width/theme with 4x prose alignment crops + keyboard Event navigation

The journey used the pre-existing job server binary, with this job's production web build. Metadata came from a real local HTML fixture through the existing test DNS/socket shim and production worker. Raster assets passed through the production image route. Notes and Events came from the real APIs and a local CalDAV provider. All screenshot contexts emulate macOS. Evidence includes 390/820/1440 px, light and dark, full metadata, no image, internal Note, internal Event, the shared hover pill and 4x crops with surrounding prose. I checked icon/text alignment in the crops. Visual review remains with Claude.

UX gaps closed: prose styles no longer underline card links; paragraph resets no longer remove card spacing; phone thumbnails no longer use space; missing or failed favicons keep a passive domain initial; Events show their real title and a readable date/time; external new-tab navigation and Event keyboard navigation are exercised.

Known gaps / UX gaps left: other route-only item kinds retain their existing fallback labels when the Notes Index has no title. The four check warnings are in unchanged AttachmentDeck, AgendaList and Notes-list CSS. No remaining gap was found in the displayed external, Note and Event journeys. Visual approval is still due.

Decisions: use ItemCard's passive content for internal cards; use its kind label as the Note meta line rather than invent an edit date; use the existing card resolver for Event metadata; use a domain initial when a favicon is absent or fails. No new design system primitive was added.

Performance: retained bench/link-previews-1151.mjs. No measurements were run, as this is a visual job under the current verification policy.

For the merge round: cd apps/web && bun run test:e2e:notes must check the full Notes interaction surface. tests/adversarial/run.sh must retain the existing link-preview and image-route boundary checks. Full workspace Rust gates stay with the merge round.

Cleanup: cargo clean completed; web build/output directories were removed. Worktree is clean. Screenshots are attached, not committed.

Review artifacts (25 attachments)

Built the rich Note link cards requested in linkcards3-1151. Head: `ee44d667245215886b9e2406e24c0c432393c3f9`. Branch: `job/linkcards-1151`. Merged `origin/dev` once. No push, deploy or publication of code. External cards use the shared card radius, glass tint and hairline frame. They have a site favicon, site name, semibold title, a two-line description and a 120 px raster thumbnail. The card is one focusable new-tab link. Phone cards use one line and hide the thumbnail. Cards keep the text column's left edge and normal block gap. Internal Note and Event cards reuse passive ItemCard content in the same frame. Events use the existing batched card resolver and the shared date/time formatter. Hover uses the existing block action pill. Files: `apps/web/src/lib/notes/NoteView.svelte`, `apps/web/src/lib/notes/inlineLinks.ts`, `apps/web/src/lib/notes/inlineLinks.test.ts`, `apps/web/e2e/linkcards-1151.mjs`, `contracts/perf/exceptions.json`, `contracts/perf/adoption-1058.json`. Performance pins match the exact live source; rules and ratchets did not change. No dependencies changed. Commits: `7fc0c290f` (card frame, metadata and Event titles), `fd112a562` (date/time formatting), `ee44d6672` (real metadata fixtures and review evidence). **Gate output (verbatim summary)** `cargo fmt --check`: exit 0, no output. No Rust crate changed, so no Rust compilation, clippy or test run was needed. `cd apps/web && bun run check`: exit 0. ``` perf-lint: PASS; 0 violations; 22109 scoped exceptions svelte-check found 0 errors and 4 warnings in 3 files ``` `cd apps/web && bun run test --maxWorkers=2`: exit 0. ``` Test Files 267 passed (267) Tests 1847 passed (1847) Start at 05:06:15 Duration 1129.65s (transform 33%, environment 25%, import 20%, tests 16%, setup 6%) ``` Focused `inlineLinks.test.ts`: 13 passed. Final production build: exit 0. Production user journey: exit 0. ``` PASS paste → Cmd+Z → block-menu card/link + Copy link → reload → pointer/keyboard/touch card navigation + live rename → keyboard Settings + Retry + Undo → offline link PASS metadata + no-image + Note + Event cards at every width/theme with 4x prose alignment crops + keyboard Event navigation ``` The journey used the pre-existing job server binary, with this job's production web build. Metadata came from a real local HTML fixture through the existing test DNS/socket shim and production worker. Raster assets passed through the production image route. Notes and Events came from the real APIs and a local CalDAV provider. All screenshot contexts emulate macOS. Evidence includes 390/820/1440 px, light and dark, full metadata, no image, internal Note, internal Event, the shared hover pill and 4x crops with surrounding prose. I checked icon/text alignment in the crops. Visual review remains with Claude. **UX gaps closed:** prose styles no longer underline card links; paragraph resets no longer remove card spacing; phone thumbnails no longer use space; missing or failed favicons keep a passive domain initial; Events show their real title and a readable date/time; external new-tab navigation and Event keyboard navigation are exercised. **Known gaps / UX gaps left:** other route-only item kinds retain their existing fallback labels when the Notes Index has no title. The four check warnings are in unchanged AttachmentDeck, AgendaList and Notes-list CSS. No remaining gap was found in the displayed external, Note and Event journeys. Visual approval is still due. **Decisions:** use ItemCard's passive content for internal cards; use its kind label as the Note meta line rather than invent an edit date; use the existing card resolver for Event metadata; use a domain initial when a favicon is absent or fails. No new design system primitive was added. **Performance:** retained `bench/link-previews-1151.mjs`. No measurements were run, as this is a visual job under the current verification policy. **For the merge round:** `cd apps/web && bun run test:e2e:notes` must check the full Notes interaction surface. `tests/adversarial/run.sh` must retain the existing link-preview and image-route boundary checks. Full workspace Rust gates stay with the merge round. Cleanup: `cargo clean` completed; web build/output directories were removed. Worktree is clean. Screenshots are attached, not committed. **Review artifacts (25 attachments)** - [link-card-hover-pill.png](https://git.kayg.org/attachments/ead9584c-c81e-40f3-8ba8-e017ccca434a) - [link-cards-1440-dark-4x.png](https://git.kayg.org/attachments/0d560f83-5acb-4dab-96be-e1202c5baafc) - [link-cards-1440-dark.png](https://git.kayg.org/attachments/060cde7e-368c-4811-a968-3269f7d470d9) - [link-cards-1440-light-4x.png](https://git.kayg.org/attachments/68d89b33-8719-4eca-b24f-296868c74e62) - [link-cards-1440-light.png](https://git.kayg.org/attachments/47fe16f1-b6a0-418a-a819-2356fadaa304) - [link-cards-390-dark-4x.png](https://git.kayg.org/attachments/83a3e7e2-2333-42cf-a5f4-b294aaf8c4ea) - [link-cards-390-dark.png](https://git.kayg.org/attachments/91140a75-c395-4bc4-a1e4-b3fa9dc87f65) - [link-cards-390-light-4x.png](https://git.kayg.org/attachments/61a798f1-5cd6-4f85-9eda-2998fdbcfd54) - [link-cards-390-light.png](https://git.kayg.org/attachments/30f92e39-6b46-462c-ab1f-cc7642b6506f) - [link-cards-820-dark-4x.png](https://git.kayg.org/attachments/e1da4d7d-6635-4a03-89d1-87ca2758419d) - [link-cards-820-dark.png](https://git.kayg.org/attachments/e7b06cd9-9ed1-41bc-b174-16bd71676482) - [link-cards-820-light-4x.png](https://git.kayg.org/attachments/6b9c6e26-308f-4358-ae7b-18a7e3e695a4) - [link-cards-820-light.png](https://git.kayg.org/attachments/adee075b-6b60-490c-b620-6fbc53dc74ed) - [link-preview-settings-1440-dark.png](https://git.kayg.org/attachments/7c215a1b-8d05-4256-ba2d-cc20beb93342) - [link-preview-settings-1440-light.png](https://git.kayg.org/attachments/3f2cf52b-520c-428c-a1b3-6535b06fefd3) - [link-preview-settings-390-dark.png](https://git.kayg.org/attachments/ee86ee30-76f8-48c1-afd2-40b92f57bbad) - [link-preview-settings-390-light.png](https://git.kayg.org/attachments/08ffaf68-b2da-4313-8a80-97c23f324459) - [link-preview-settings-820-dark.png](https://git.kayg.org/attachments/ee3c90a4-faf2-4fee-aa99-21bcd3cb93fc) - [link-preview-settings-820-light.png](https://git.kayg.org/attachments/d20bae44-a45b-4c14-a49a-e493ebd9c837) - [link-preview-settings-error-1440-dark.png](https://git.kayg.org/attachments/f146c749-9a2e-411c-89bb-b8f3eca1d172) - [link-preview-settings-error-1440-light.png](https://git.kayg.org/attachments/72af7040-1164-48ed-849d-05a00bf8c821) - [link-preview-settings-error-390-dark.png](https://git.kayg.org/attachments/6037d8fc-cd33-42bf-bd54-2cbb4a3b93c3) - [link-preview-settings-error-390-light.png](https://git.kayg.org/attachments/45bc2317-48d8-4e7d-abd1-69a0317c1d3e) - [link-preview-settings-error-820-dark.png](https://git.kayg.org/attachments/831ed773-58c0-4a3e-a134-0bbe8b9d24b2) - [link-preview-settings-error-820-light.png](https://git.kayg.org/attachments/3adf466e-2411-41a6-b04e-b6209b8cc03b)
Author
Owner

Follow-up from #1151 review: internal Event card meta shows "10/6/26 at 10:00 AM"; use the shared locale date formatter (packages/ui/src/time.ts) as in the hover card ("Tue, 6 Oct · 10:00"). Internal cards should match the external card padding exactly (4x crop).

Follow-up from #1151 review: internal Event card meta shows "10/6/26 at 10:00 AM"; use the shared locale date formatter (packages/ui/src/time.ts) as in the hover card ("Tue, 6 Oct · 10:00"). Internal cards should match the external card padding exactly (4x crop).
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#1151
No description provided.