CANVAS: live cards for any calternal item, placeholders, backlinks (§60) #977

Open
opened 2026-10-03 06:38:52 +00:00 by kayg · 26 comments
Owner

Owner decisions (2026-10-03, #509 grill Q3)

Contract: docs/DESIGN.md §60 "Live cards" and "Adding items". Depends on the Canvas core issue (build on its branch once merged) and the shared ItemCard family (#822).
"We need to utilise the live nature of excalidraw more."

Scope

  • Any calternal item dropped or pasted onto a canvas becomes a live card by default, switchable to a plain link per item. Item kinds: Mail message or thread, Money transaction, account or category, Event, Log entry, Task, Contact, Note, heading or block, File, folder, photo, saved search, Settings section, any §33 deep link, external URL.
  • A card is an Excalidraw embeddable element that stores the deep link (stable identity, never a path) and is drawn with the shared ItemCard family. Do not build a new card component.
  • Cards update live when the item changes (reuse the existing change feed or SSE; batch updates; render only cards in view).
  • Click opens the item or the anchored inspector (Cmd+I). A deleted item, or one the viewer cannot read, shows a plain placeholder and leaks nothing: no title, no counts.
  • Adding: drag from any calternal list, sidebar or search result; paste a calternal link; a "+" picker that searches every plugin (reuse the palette/search); drop files and photos (upload through Files).
  • Backlinks: items on a canvas show "On " in backlinks and the Inspector.
  • Parity: API, CLI, MCP and WebMCP can add and remove cards through the same event path.

Security and isolation

Card data is resolved server-side per viewer. Add a cross-user isolation matrix case (#472): User B opens a canvas shared by User A with cards for A's private Mail and Money and sees only placeholders.

Performance

Bench profile: a canvas with 300 live cards, open and pan, and an update storm on 50 card items. Load card data lazily for visible cards.

Verification

Production-build screenshots (390/820/1440, light and dark) of each card kind, placeholder, and link mode. E2E as a User: drag a Task from Tasks onto a canvas, complete the Task elsewhere, and see the card update; switch a card to a link; open a card.

## Owner decisions (2026-10-03, #509 grill Q3) Contract: `docs/DESIGN.md` §60 "Live cards" and "Adding items". Depends on the Canvas core issue (build on its branch once merged) and the shared ItemCard family (#822). "We need to utilise the live nature of excalidraw more." ## Scope - Any calternal item dropped or pasted onto a canvas becomes a **live card** by default, switchable to a plain **link** per item. Item kinds: Mail message or thread, Money transaction, account or category, Event, Log entry, Task, Contact, Note, heading or block, File, folder, photo, saved search, Settings section, any §33 deep link, external URL. - A card is an Excalidraw `embeddable` element that stores the deep link (stable identity, never a path) and is drawn with the shared ItemCard family. Do not build a new card component. - Cards update live when the item changes (reuse the existing change feed or SSE; batch updates; render only cards in view). - Click opens the item or the anchored inspector (Cmd+I). A deleted item, or one the viewer cannot read, shows a plain placeholder and leaks nothing: no title, no counts. - Adding: drag from any calternal list, sidebar or search result; paste a calternal link; a "+" picker that searches every plugin (reuse the palette/search); drop files and photos (upload through Files). - Backlinks: items on a canvas show "On <canvas name>" in backlinks and the Inspector. - Parity: API, CLI, MCP and WebMCP can add and remove cards through the same event path. ## Security and isolation Card data is resolved server-side per viewer. Add a cross-user isolation matrix case (#472): User B opens a canvas shared by User A with cards for A's private Mail and Money and sees only placeholders. ## Performance Bench profile: a canvas with 300 live cards, open and pan, and an update storm on 50 card items. Load card data lazily for visible cards. ## Verification Production-build screenshots (390/820/1440, light and dark) of each card kind, placeholder, and link mode. E2E as a User: drag a Task from Tasks onto a canvas, complete the Task elsewhere, and see the card update; switch a card to a link; open a card.
Author
Owner

Security requirements (design review 2026-10-03): must be met

Canvas (§60/§61, #976/#977): defensive design review

Read-only review, 2026-10-03. Inputs: CLAUDE.md, DESIGN §33, §37, §54, §60, §61,
issues #976 and #977, crates/calternal-server/src/security.rs,
crates/plugins/files/src/user_bytes.rs, crates/calternal-collab/src/{session,stored}.rs.

0. Current state that matters

  • Shell CSP (security.rs:121): frame-src 'none', connect-src 'self',
    font-src 'self' data:, img-src 'self' data: blob: https:,
    frame-ancestors 'none', plus X-Frame-Options: DENY. The module doc says
    "the app has no <iframe> of its own". Stock Excalidraw embeddables
    (iframes) cannot render under this CSP
    , and they must not: keep
    frame-src 'none' for v1.
  • img-src https: lets any https: image load directly from the viewer's
    browser (IP/UA leak, tracking pixel). A canvas makes this attacker-controlled
    by collaborators. Card images must go through a server proxy (see §2).
  • User bytes are served with SANDBOX_POLICY (sandbox; default-src 'none').
    SVG exports must use this path.
  • Collab today (Notes): MAX_MESSAGE_BYTES = 12 MiB, MAX_STORED_STATE = 16 MiB,
    share recheck every 500 ms, public-edit frame cap 3 MiB and 300 frames/min,
    "update must still convert to Markdown, else roll back and disconnect",
    awareness clocks near u32::MAX refused. Reuse all of it; the Canvas needs
    a canvas-specific validator in place of the Markdown check.
  • Inconsistency to flag: session.rs has a public Edit grant
    (PublicEditAccess), but §54 says "Public links are view only". A canvas
    must not inherit public edit. Decide/record before #976 merges.

1. Untrusted file input (server parse + client restore)

The same hostile bytes reach three parsers: the server (search, backlinks,
room load), Excalidraw restore() in every viewer's browser, and other tools
over WebDAV. Validate on the server at load and on every Yjs update, so one
hostile collaborator cannot freeze every other viewer.

Requirements (numbers are starting points; record the final ones in §60):

Limit Value Why
File bytes (.excalidraw, .excalidraw.md) 32 MiB read cap, streamed memory
JSON nesting depth 64 (serde_json default 128 is too deep for customData) stack
Elements per scene 20 000 (bench needs 5k) render / Yjs map size
Points per linear/freedraw element 20 000; total points per scene 2 M rough.js / path cost
Coordinates, width, height finite, abs <= 1e7; -0, NaN, Inf, 1e308 rejected rough.js hachure line count = size/gap: huge shapes hang every viewer
strokeWidth, fontSize, roughness, opacity, angle finite, clamped ranges same
Text per element / per scene 64 KiB / 4 MiB layout cost
Element ID, fileId [A-Za-z0-9_-]{1,64} Y.Map keys, selectors, deep links
Fractional index (index) <= 64 chars, valid base-62 unbounded growth by repeated insert-between
Unknown fields per element (byte-preserved) <= 16 KiB opaque preservation without unbounded blobs
files (dataURL images) mime allowlist png/jpeg/webp/gif/svg+xml; base64 must decode; magic bytes match mime; <= 10 MiB each, counts against quota bombs, disguised types
compressed-json (LZ-String base64) decode with an output cap (equal to the file cap) and a work budget LZW output can grow quadratically with input
PNG/SVG embedded scene on import zlib/pako inflate with output cap inflate bomb
.excalidraw.md sections one linear pass; first json/compressed-json block only; fence tricks ( ````, %%, nested fences) cannot hide a second scene parser differential

Other rules:

  • Opening never writes (#661): a file that fails validation opens read-only
    with a real error state. The server never "repairs" and saves it.
  • Parser differential: the server and Excalidraw must agree on what is visible.
    Index only text that Excalidraw renders (skip isDeleted: true, skip text
    bound to a deleted container). Duplicate JSON keys: reject (serde
    Value keeps the last, JSON.parse keeps the last, but other tools differ).
    Lone surrogates, BOM, overlong numbers: open read-only, never 5xx.
  • Prototype pollution: reject keys __proto__, constructor, prototype in
    element IDs, files keys, appState, customData and Y.Map keys. Client
    code that turns Y.Map into objects must use Object.create(null)/Map.
  • Text tricks: render bidi controls (U+202A–U+202E, U+2066–U+2069) and
    zero-width chars (U+200B–U+200D, U+2060, U+FEFF) visibly in the canvas
    title, card labels and link tooltips; reuse the existing name sanitiser
    (strip_unsafe_name_chars) for the file name / title. External link hover
    shows the punycode host for mixed-script hosts.
  • Element link: allow only https:, http:, mailto: and same-origin
    calternal paths that start with exactly one / (not //, not /\).
    Reject javascript:, data:, vbscript:, file:, blob:. Open external
    links with noopener noreferrer. Check on the server (update validator)
    and in the client.
  • Images: keep image bytes out of Yjs. Store them as content-addressed
    attachments through Files/calternal-fs and put only the fileId in the
    element. Otherwise every image lands in the §61 history log forever.
    Plain .excalidraw files with inline dataURLs keep them byte-preserved on
    disk, but the room holds a reference.
  • Never decode images on the server outside the media sandbox. Client decode
    of a 100k x 100k PNG is a viewer-side DoS: check declared dimensions from
    the header before upload is accepted.
  • Excalidraw network: self-host fonts and assets (EXCALIDRAW_ASSET_PATH),
    remove library browsing (libraries.excalidraw.com), share/collab links
    (json.excalidraw.com) and any Excalidraw+ promotion. The CSP already blocks
    them; remove them so nothing fails visibly.
  • Mermaid conversion: run in a dedicated Web Worker with securityLevel: 'strict', input cap (64 KiB), and terminate on a time budget. If API/CLI/MCP
    need it server-side, run it in the existing sandbox, never in the server
    process.

2. Embeddables and external URLs (privacy by default)

Risks of stock Excalidraw embeddables: third-party tracking on open (cookies,
IP, referrer), a collaborator-placed page that looks like calternal inside
calternal chrome (credential phishing in a trusted origin), clickjacking of
the embedded page, autoplay, allow-same-origin allow-scripts allow-popups
in Excalidraw's default sandbox, and a CSP hole if frame-src is opened.

Recommendation ("normie friendly, privacy invisible"):

  1. No third-party iframes in v1. Keep frame-src 'none'. Set
    validateEmbeddable={false} for foreign URLs and draw every embeddable
    through renderEmbeddable (React, same document) with the ItemCard family.
  2. External URL = server-fetched preview card: title, site name, favicon,
    og:image. Fetch with the #431 SSRF guard (DNS pinning, no private,
    loopback, link-local, CGNAT or metadata ranges, re-check every redirect,
    max 3 redirects), no cookies, fixed UA, 5 s timeout, 1 MiB HTML cap,
    text/html only, image cap 2 MiB and decoded in the media sandbox. Cache
    per owner. Images are served from the calternal origin (proxy), so the
    viewer's browser never contacts the third party.
  3. Only editors trigger a fetch (when adding or refreshing a card). A viewer,
    a share recipient or a public link never causes an outgoing request
    (no SSRF amplifier, no "who opened this" signal to the site).
  4. Video embeds (YouTube/Vimeo) are a later, separate decision: if built, use
    a click-to-play facade (cached thumbnail), then youtube-nocookie.com /
    player.vimeo.com?dnt=1 only, sandbox="allow-scripts allow-same-origin allow-presentation", referrerpolicy="no-referrer", and add only those
    two hosts to frame-src on the shell. Not part of #976/#977.
  5. Card click on an external URL opens a new tab (noopener noreferrer) and
    shows the real host before navigation; never navigate the app frame.
  6. Consider removing https: from shell img-src once card images are
    proxied (separate issue; check Mail remote images and other users first).

3. Live cards and isolation (#977)

  • The file stores only the deep link (§33 stable identity) and the card
    mode. Never write a resolved title, amount, snippet or thumbnail into the
    element, customData, the text section or the links section. The file is
    read by WebDAV clients, exports, search and every recipient.
  • Resolution is server-side per viewer, through one batch endpoint
    (POST with up to 200 links, rate limited per User). Each link resolves
    under the viewer's own authority: own item, item shared to the viewer
    (§54), or placeholder.
  • Placeholder is uniform: same response shape, same status, same size class
    and no timing difference for "deleted", "never existed", "not yours" and
    "malformed". No title, no count, no kind-specific icon beyond what the
    link text already shows. (§54: non-recipients get 404 with no timing or
    size difference.)
  • Deep links must be opaque IDs. Check the §33 grammar: /search?q=… puts
    query text into the canvas file; /mail/m/<message-id> must be calternal's
    ID, not an RFC Message-ID that contains an address. A card for a search
    query stores a saved-search ID or warns that the query text is visible to
    collaborators.
  • The live change feed for cards subscribes per viewer and only to IDs the
    viewer can read; a revoked share turns the card into a placeholder within
    the existing recheck interval (500 ms).
  • Enumeration: the batch resolver is an oracle only for what the viewer
    can already read. Keep it so: no existence bit, cap batch size, rate limit,
    log bursts. Element-link ?el= is opaque: never put it into a CSS selector
    or HTML; look it up in the element map.
  • Search: index the canvas's own text only, never resolved card content
    (else a recipient's search for "salary" matches the canvas through the
    owner's Money card).
  • Backlinks "On ": show a canvas in an item's backlinks only
    when the viewer can read that canvas. A hostile User can put cards that
    point at another User's item IDs; that must not create backlinks, counts or
    notices in the item owner's view (spam and canvas-name leak).
  • Shared canvas (Share/Collaborate): recipients see placeholders for the
    owner's private items. Collaborators can add cards for their own items;
    the owner then sees placeholders for those unless shared to the owner.
  • Public link (/s/<slug>): resolve as an anonymous viewer: every
    calternal item is a placeholder; external cards show the cached preview
    from the proxy. No live feed, no edit (see the public-edit note in §0).
  • Export renders cards as the exporting User sees them; the embedded
    scene in the export carries only links. Agents/MCP get the same per-viewer
    resolution as the web (no "raw" read that bypasses it).

4. Collaboration path (one event path)

  • Per-frame authz: reuse the 500 ms recheck. Viewers (Share) may receive
    sync and send SyncStep1 and awareness only; drop and disconnect on
    Update/SyncStep2 from a Viewer
    . API/CLI/MCP/WebMCP writes go through
    the same room and the same check.
  • Author binding (§61): the author of an update comes from the
    authenticated connection, never from the update contents. Each connection
    gets its own Yjs clientIDs; reject an update with structs under a clientID
    that belongs to another connection or author. Without this, a collaborator
    can spoof history attribution and make per-author undo revert someone
    else's work, or corrupt convergence by clientID reuse.
  • Update validator (replaces "still converts to Markdown"): after applying
    in a transaction, every changed element passes the §1 schema; else roll
    back and disconnect.
  • Pending structs: an update with missing dependencies is held by yrs in a
    pending store. Cap pending bytes per room (for example 1 MiB) and
    disconnect when exceeded, or a client can grow server memory without
    bound.
  • Clock abuse: reject clocks/lengths near u32::MAX/2^53, GC/skip ranges
    longer than the document, and delete sets that name clients the room has
    never seen. Fuzz yrs decode with these (it must error, never panic or
    allocate by declared length).
  • Sizes: per-frame cap for canvas lower than Notes (2 MiB, since images are
    out of Yjs); room state cap reuse 16 MiB; awareness state <= 8 KiB per
    client, server stamps name/colour from the session (no spoofed cursor
    labels), awareness rate cap.
  • Rates: per connection and per author frames/min (reuse the 300/min
    bucket), per User open rooms and connections (reuse client_stream_permit).
  • Mass delete: legitimate for an editor, so authz + history is the defence.
    Restore must handle "all elements deleted" in one update; add a test.
  • History growth DoS (§61): coalesce updates per author over 1–2 s before
    append; history bytes count against the owner's quota; per
    collaborator daily write budget so a recipient cannot fill the owner's
    disk; snapshot + fold by the retention rule; open history without full
    replay (already a §61 rule). A move-storm (one element dragged 300
    frames/min for an hour) is a bench and adversarial case.
  • Flush: debounced writes go through calternal-fs atomic replace; a
    concurrent WebDAV write of the same file triggers the existing reload
    path, never a silent overwrite (sync collision = merge blocker).

5. Export (SVG/PNG with embedded scene)

  • Excalidraw's SVG export wraps linked elements in <a href> and can emit
    <foreignObject> for embeddables and @font-face data URLs in <style>.
    Post-process every SVG export on a strict allowlist: no <script>, no
    on* attributes, no <foreignObject>, no <iframe>/<embed>/<object>,
    href only #…, data:image/(png|jpeg|webp|gif), https:, http:,
    mailto:; no external <use>, <image> or CSS url() to remote hosts;
    fonts only as embedded data URLs.
  • Serve exports and any .svg from Home with SANDBOX_POLICY and
    nosniff; show them in <img>, never inline in the app DOM.
  • The embedded scene payload (SVG metadata, PNG tEXt/iTXt) is untrusted
    on re-import: same §1 limits, inflate cap, chunk size cap (16 MiB).
  • The embedded scene carries links only, never resolved card data (§3).
  • PNG export of a huge scene: cap the export canvas size (browser max
    canvas area) and the scale; fail with a message, not a tab crash.

6. Adversarial cases for tests/adversarial/ (new canvas_probe)

File input (write via WebDAV and the API, then open, search, backlinks):

  1. 200 MiB .excalidraw → refused/streamed, no OOM, no 5xx.
  2. JSON nested 100 000 deep in customData → read-only error, server alive.
  3. 1 M elements; 1 element with 5 M freedraw points.
  4. Rectangle with width 1e308, x NaN (as 1e999), negative-zero, -1e308;
    hachure fill with roughness 0 and tiny gap → rejected server-side; second
    browser context stays responsive (Playwright INP check).
  5. LZ-String bomb: 1 MiB compressed-json that expands past the cap.
  6. PNG with a pako-compressed scene bomb in tEXt; SVG with a 50 MiB metadata payload.
  7. files with mime image/png but HTML/SVG-with-script bytes; base64 garbage; 11 MiB image; 1 000 images.
  8. Keys __proto__, constructor, prototype as element ID, fileId, appState key; check Object.prototype is clean in the page afterwards.
  9. Duplicate keys, lone surrogate \ud800, BOM, trailing garbage, two json blocks, fence-in-fence in .excalidraw.md.
  10. Element link = javascript:alert(1), JaVaScRiPt:, \x01javascript:, //evil.example, /\evil.example, data:text/html,….
  11. Text with U+202E, zero-width joiners, homoglyph host xn-- link; file name gpj.exe‮oard.excalidraw.md.
  12. Element IDs 10 KiB long, with ", ], </script>; fractional index 1 MiB long.
  13. Opening every hostile file leaves its bytes unchanged on disk (#661).

Collaboration (WebSocket, two Users + one Viewer):
14. Viewer sends Update and SyncStep2 → dropped, disconnected, document unchanged.
15. Share revoked mid-session → next frame refused within 500 ms.
16. Update using another connection's clientID → refused; history author unchanged.
17. Update with missing dependencies repeated until the pending cap → disconnect, RSS flat.
18. Clock 0xFFFFFFFF, struct length 2^53, delete set over 2^32 range, unknown clients.
19. Truncated/garbage varints, 2 MiB + 1 frame, 10 000 tiny frames/min (rate limit).
20. Valid update that sets an element to an invalid schema value (NaN, 1e308) → rolled back, sender disconnected.
21. Awareness 1 MiB, spoofed user name, clock near max.
22. Move-storm for 10 minutes: history bytes and RSS bounded; quota charged to the owner; collaborator daily budget enforced.
23. Mass delete of 5 000 elements, then Restore.
24. Concurrent WebDAV PUT of the file during a live session → no lost edits, no 5xx.
25. Public link to a canvas: WebSocket with an Edit token → refused (unless §54 is changed).

Cards and embeds:
26. Card pointing at http://127.0.0.1, 169.254.169.254, [::1], DNS-rebind host (reuse dns_rebind_preload.c), redirect to private IP → no fetch.
27. Preview HTML 50 MiB, slowloris server, og:image 100k x 100k PNG.
28. Viewer opens a canvas with external cards → zero outgoing requests from the server and from the browser to third-party hosts (network log).
29. ?el= with "] <img onerror> and 10 KiB value → no injection, canvas opens.
30. SVG export of an element with javascript: link and an embeddable → output passes the allowlist; served with sandbox CSP.
31. Mermaid input of 10 MiB and a pathological diagram → Worker killed by budget, UI usable.

7. Isolation matrix cases (#472/#331)

New routes to classify: canvas room WebSocket, canvas create/update/export
API, card batch resolver, card live feed, URL preview fetch/proxy, image
attachment upload/read, element-link resolution, history list/restore/undo.

Cases (User A owner, User B recipient or stranger, anonymous public):

  • M1 B (no share) opens A's canvas by calternal-id, by ?el=, via room WS, via export, via history → 404 identical to a missing ID.
  • M2 B with Share sees A's canvas; cards for A's Mail, Money, Contacts, Tasks, private Notes are placeholders; batch resolver returns uniform placeholders; timing/size match a missing ID.
  • M3 B with Collaborate adds a card for B's own Money item; A sees a placeholder.
  • M4 B places cards with A's item IDs on B's canvas → A's backlinks/Inspector show nothing; A gets no notice.
  • M5 Search as B (Share) for a word only in A's Money card title → no hit on the canvas.
  • M6 Public link /s/<slug>: every calternal card is a placeholder, no live feed, no WS write, no preview fetch triggered.
  • M7 Revoke Share during a live session → WS closed, card feed closed, placeholders.
  • M8 B's history view/per-author undo cannot reveal or revert another canvas, and undo of B's turn cannot remove A's edits made later.
  • M9 Preview proxy: B cannot read A's cached preview by URL hash or ID unless B can read a canvas that contains it.
  • M10 Image attachment fileId of A's canvas fetched by B directly → 404.
  • M11 Export by B (Share) embeds links only; resolved data is B's view only.
  • M12 MCP/CLI/WebMCP as B: same results as M1–M11 (one event path).
## Security requirements (design review 2026-10-03): must be met # Canvas (§60/§61, #976/#977): defensive design review Read-only review, 2026-10-03. Inputs: CLAUDE.md, DESIGN §33, §37, §54, §60, §61, issues #976 and #977, `crates/calternal-server/src/security.rs`, `crates/plugins/files/src/user_bytes.rs`, `crates/calternal-collab/src/{session,stored}.rs`. ## 0. Current state that matters - Shell CSP (security.rs:121): `frame-src 'none'`, `connect-src 'self'`, `font-src 'self' data:`, `img-src 'self' data: blob: https:`, `frame-ancestors 'none'`, plus `X-Frame-Options: DENY`. The module doc says "the app has no `<iframe>` of its own". **Stock Excalidraw embeddables (iframes) cannot render under this CSP**, and they must not: keep `frame-src 'none'` for v1. - `img-src https:` lets any `https:` image load directly from the viewer's browser (IP/UA leak, tracking pixel). A canvas makes this attacker-controlled by collaborators. Card images must go through a server proxy (see §2). - User bytes are served with `SANDBOX_POLICY` (`sandbox; default-src 'none'`). SVG exports must use this path. - Collab today (Notes): `MAX_MESSAGE_BYTES` = 12 MiB, `MAX_STORED_STATE` = 16 MiB, share recheck every 500 ms, public-edit frame cap 3 MiB and 300 frames/min, "update must still convert to Markdown, else roll back and disconnect", awareness clocks near `u32::MAX` refused. Reuse all of it; the Canvas needs a canvas-specific validator in place of the Markdown check. - Inconsistency to flag: `session.rs` has a **public Edit** grant (`PublicEditAccess`), but §54 says "Public links are view only". A canvas must not inherit public edit. Decide/record before #976 merges. ## 1. Untrusted file input (server parse + client restore) The same hostile bytes reach three parsers: the server (search, backlinks, room load), Excalidraw `restore()` in every viewer's browser, and other tools over WebDAV. Validate on the server at load **and on every Yjs update**, so one hostile collaborator cannot freeze every other viewer. Requirements (numbers are starting points; record the final ones in §60): | Limit | Value | Why | |---|---|---| | File bytes (`.excalidraw`, `.excalidraw.md`) | 32 MiB read cap, streamed | memory | | JSON nesting depth | 64 (serde_json default 128 is too deep for `customData`) | stack | | Elements per scene | 20 000 (bench needs 5k) | render / Yjs map size | | Points per linear/freedraw element | 20 000; total points per scene 2 M | rough.js / path cost | | Coordinates, width, height | finite, abs <= 1e7; `-0`, NaN, Inf, 1e308 rejected | rough.js hachure line count = size/gap: huge shapes hang every viewer | | strokeWidth, fontSize, roughness, opacity, angle | finite, clamped ranges | same | | Text per element / per scene | 64 KiB / 4 MiB | layout cost | | Element ID, fileId | `[A-Za-z0-9_-]{1,64}` | Y.Map keys, selectors, deep links | | Fractional index (`index`) | <= 64 chars, valid base-62 | unbounded growth by repeated insert-between | | Unknown fields per element (byte-preserved) | <= 16 KiB opaque | preservation without unbounded blobs | | `files` (dataURL images) | mime allowlist png/jpeg/webp/gif/svg+xml; base64 must decode; magic bytes match mime; <= 10 MiB each, counts against quota | bombs, disguised types | | compressed-json (LZ-String base64) | decode with an **output cap** (equal to the file cap) and a work budget | LZW output can grow quadratically with input | | PNG/SVG embedded scene on import | zlib/pako inflate with output cap | inflate bomb | | `.excalidraw.md` sections | one linear pass; first `json`/`compressed-json` block only; fence tricks (```` ```` ````, `%%`, nested fences) cannot hide a second scene | parser differential | Other rules: - **Opening never writes** (#661): a file that fails validation opens read-only with a real error state. The server never "repairs" and saves it. - Parser differential: the server and Excalidraw must agree on what is visible. Index only text that Excalidraw renders (skip `isDeleted: true`, skip text bound to a deleted container). Duplicate JSON keys: reject (serde `Value` keeps the last, JSON.parse keeps the last, but other tools differ). Lone surrogates, BOM, overlong numbers: open read-only, never 5xx. - Prototype pollution: reject keys `__proto__`, `constructor`, `prototype` in element IDs, `files` keys, `appState`, `customData` and Y.Map keys. Client code that turns Y.Map into objects must use `Object.create(null)`/`Map`. - Text tricks: render bidi controls (U+202A–U+202E, U+2066–U+2069) and zero-width chars (U+200B–U+200D, U+2060, U+FEFF) visibly in the canvas title, card labels and link tooltips; reuse the existing name sanitiser (`strip_unsafe_name_chars`) for the file name / title. External link hover shows the punycode host for mixed-script hosts. - Element `link`: allow only `https:`, `http:`, `mailto:` and same-origin calternal paths that start with exactly one `/` (not `//`, not `/\`). Reject `javascript:`, `data:`, `vbscript:`, `file:`, `blob:`. Open external links with `noopener noreferrer`. Check on the server (update validator) and in the client. - Images: keep image bytes **out of Yjs**. Store them as content-addressed attachments through Files/`calternal-fs` and put only the fileId in the element. Otherwise every image lands in the §61 history log forever. Plain `.excalidraw` files with inline dataURLs keep them byte-preserved on disk, but the room holds a reference. - Never decode images on the server outside the media sandbox. Client decode of a 100k x 100k PNG is a viewer-side DoS: check declared dimensions from the header before upload is accepted. - Excalidraw network: self-host fonts and assets (`EXCALIDRAW_ASSET_PATH`), remove library browsing (libraries.excalidraw.com), share/collab links (json.excalidraw.com) and any Excalidraw+ promotion. The CSP already blocks them; remove them so nothing fails visibly. - Mermaid conversion: run in a dedicated Web Worker with `securityLevel: 'strict'`, input cap (64 KiB), and terminate on a time budget. If API/CLI/MCP need it server-side, run it in the existing sandbox, never in the server process. ## 2. Embeddables and external URLs (privacy by default) Risks of stock Excalidraw embeddables: third-party tracking on open (cookies, IP, referrer), a collaborator-placed page that looks like calternal inside calternal chrome (credential phishing in a trusted origin), clickjacking of the embedded page, autoplay, `allow-same-origin allow-scripts allow-popups` in Excalidraw's default sandbox, and a CSP hole if `frame-src` is opened. Recommendation ("normie friendly, privacy invisible"): 1. **No third-party iframes in v1.** Keep `frame-src 'none'`. Set `validateEmbeddable={false}` for foreign URLs and draw every embeddable through `renderEmbeddable` (React, same document) with the ItemCard family. 2. **External URL = server-fetched preview card**: title, site name, favicon, og:image. Fetch with the #431 SSRF guard (DNS pinning, no private, loopback, link-local, CGNAT or metadata ranges, re-check every redirect, max 3 redirects), no cookies, fixed UA, 5 s timeout, 1 MiB HTML cap, `text/html` only, image cap 2 MiB and decoded in the media sandbox. Cache per owner. Images are served from the calternal origin (proxy), so the viewer's browser never contacts the third party. 3. Only editors trigger a fetch (when adding or refreshing a card). A viewer, a share recipient or a public link never causes an outgoing request (no SSRF amplifier, no "who opened this" signal to the site). 4. Video embeds (YouTube/Vimeo) are a later, separate decision: if built, use a click-to-play facade (cached thumbnail), then `youtube-nocookie.com` / `player.vimeo.com?dnt=1` only, `sandbox="allow-scripts allow-same-origin allow-presentation"`, `referrerpolicy="no-referrer"`, and add only those two hosts to `frame-src` on the shell. Not part of #976/#977. 5. Card click on an external URL opens a new tab (`noopener noreferrer`) and shows the real host before navigation; never navigate the app frame. 6. Consider removing `https:` from shell `img-src` once card images are proxied (separate issue; check Mail remote images and other users first). ## 3. Live cards and isolation (#977) - **The file stores only the deep link** (§33 stable identity) and the card mode. Never write a resolved title, amount, snippet or thumbnail into the element, `customData`, the text section or the links section. The file is read by WebDAV clients, exports, search and every recipient. - Resolution is server-side **per viewer**, through one batch endpoint (`POST` with up to 200 links, rate limited per User). Each link resolves under the viewer's own authority: own item, item shared to the viewer (§54), or placeholder. - Placeholder is uniform: same response shape, same status, same size class and no timing difference for "deleted", "never existed", "not yours" and "malformed". No title, no count, no kind-specific icon beyond what the link text already shows. (§54: non-recipients get 404 with no timing or size difference.) - Deep links must be opaque IDs. Check the §33 grammar: `/search?q=…` puts query text into the canvas file; `/mail/m/<message-id>` must be calternal's ID, not an RFC Message-ID that contains an address. A card for a search query stores a saved-search ID or warns that the query text is visible to collaborators. - The live change feed for cards subscribes per viewer and only to IDs the viewer can read; a revoked share turns the card into a placeholder within the existing recheck interval (500 ms). - **Enumeration:** the batch resolver is an oracle only for what the viewer can already read. Keep it so: no existence bit, cap batch size, rate limit, log bursts. Element-link `?el=` is opaque: never put it into a CSS selector or HTML; look it up in the element map. - **Search:** index the canvas's own text only, never resolved card content (else a recipient's search for "salary" matches the canvas through the owner's Money card). - **Backlinks "On <canvas name>":** show a canvas in an item's backlinks only when the viewer can read that canvas. A hostile User can put cards that point at another User's item IDs; that must not create backlinks, counts or notices in the item owner's view (spam and canvas-name leak). - **Shared canvas (Share/Collaborate):** recipients see placeholders for the owner's private items. Collaborators can add cards for their own items; the owner then sees placeholders for those unless shared to the owner. - **Public link (`/s/<slug>`):** resolve as an anonymous viewer: every calternal item is a placeholder; external cards show the cached preview from the proxy. No live feed, no edit (see the public-edit note in §0). - **Export** renders cards as the exporting User sees them; the embedded scene in the export carries only links. Agents/MCP get the same per-viewer resolution as the web (no "raw" read that bypasses it). ## 4. Collaboration path (one event path) - Per-frame authz: reuse the 500 ms recheck. Viewers (Share) may receive sync and send SyncStep1 and awareness only; **drop and disconnect on `Update`/`SyncStep2` from a Viewer**. API/CLI/MCP/WebMCP writes go through the same room and the same check. - **Author binding (§61):** the author of an update comes from the authenticated connection, never from the update contents. Each connection gets its own Yjs clientIDs; reject an update with structs under a clientID that belongs to another connection or author. Without this, a collaborator can spoof history attribution and make per-author undo revert someone else's work, or corrupt convergence by clientID reuse. - Update validator (replaces "still converts to Markdown"): after applying in a transaction, every changed element passes the §1 schema; else roll back and disconnect. - Pending structs: an update with missing dependencies is held by yrs in a pending store. Cap pending bytes per room (for example 1 MiB) and disconnect when exceeded, or a client can grow server memory without bound. - Clock abuse: reject clocks/lengths near `u32::MAX`/`2^53`, GC/skip ranges longer than the document, and delete sets that name clients the room has never seen. Fuzz yrs decode with these (it must error, never panic or allocate by declared length). - Sizes: per-frame cap for canvas lower than Notes (2 MiB, since images are out of Yjs); room state cap reuse 16 MiB; awareness state <= 8 KiB per client, server stamps name/colour from the session (no spoofed cursor labels), awareness rate cap. - Rates: per connection and per author frames/min (reuse the 300/min bucket), per User open rooms and connections (reuse `client_stream_permit`). - Mass delete: legitimate for an editor, so authz + history is the defence. Restore must handle "all elements deleted" in one update; add a test. - **History growth DoS (§61):** coalesce updates per author over 1–2 s before append; history bytes count against the **owner's** quota; per collaborator daily write budget so a recipient cannot fill the owner's disk; snapshot + fold by the retention rule; open history without full replay (already a §61 rule). A move-storm (one element dragged 300 frames/min for an hour) is a bench and adversarial case. - Flush: debounced writes go through `calternal-fs` atomic replace; a concurrent WebDAV write of the same file triggers the existing reload path, never a silent overwrite (sync collision = merge blocker). ## 5. Export (SVG/PNG with embedded scene) - Excalidraw's SVG export wraps linked elements in `<a href>` and can emit `<foreignObject>` for embeddables and `@font-face` data URLs in `<style>`. Post-process every SVG export on a strict allowlist: no `<script>`, no `on*` attributes, no `<foreignObject>`, no `<iframe>`/`<embed>`/`<object>`, `href` only `#…`, `data:image/(png|jpeg|webp|gif)`, `https:`, `http:`, `mailto:`; no external `<use>`, `<image>` or CSS `url()` to remote hosts; fonts only as embedded data URLs. - Serve exports and any `.svg` from Home with `SANDBOX_POLICY` and `nosniff`; show them in `<img>`, never inline in the app DOM. - The embedded scene payload (SVG metadata, PNG `tEXt`/`iTXt`) is untrusted on re-import: same §1 limits, inflate cap, chunk size cap (16 MiB). - The embedded scene carries links only, never resolved card data (§3). - PNG export of a huge scene: cap the export canvas size (browser max canvas area) and the scale; fail with a message, not a tab crash. ## 6. Adversarial cases for `tests/adversarial/` (new `canvas_probe`) File input (write via WebDAV and the API, then open, search, backlinks): 1. 200 MiB `.excalidraw` → refused/streamed, no OOM, no 5xx. 2. JSON nested 100 000 deep in `customData` → read-only error, server alive. 3. 1 M elements; 1 element with 5 M freedraw points. 4. Rectangle with width 1e308, x NaN (as `1e999`), negative-zero, `-1e308`; hachure fill with roughness 0 and tiny gap → rejected server-side; second browser context stays responsive (Playwright INP check). 5. LZ-String bomb: 1 MiB compressed-json that expands past the cap. 6. PNG with a pako-compressed scene bomb in `tEXt`; SVG with a 50 MiB metadata payload. 7. `files` with mime `image/png` but HTML/SVG-with-script bytes; base64 garbage; 11 MiB image; 1 000 images. 8. Keys `__proto__`, `constructor`, `prototype` as element ID, fileId, appState key; check `Object.prototype` is clean in the page afterwards. 9. Duplicate keys, lone surrogate `\ud800`, BOM, trailing garbage, two json blocks, fence-in-fence in `.excalidraw.md`. 10. Element `link` = `javascript:alert(1)`, `JaVaScRiPt:`, `\x01javascript:`, `//evil.example`, `/\evil.example`, `data:text/html,…`. 11. Text with U+202E, zero-width joiners, homoglyph host `xn--` link; file name `gpj.exe‮oard.excalidraw.md`. 12. Element IDs 10 KiB long, with `"`, `]`, `</script>`; fractional index 1 MiB long. 13. Opening every hostile file leaves its bytes unchanged on disk (#661). Collaboration (WebSocket, two Users + one Viewer): 14. Viewer sends `Update` and `SyncStep2` → dropped, disconnected, document unchanged. 15. Share revoked mid-session → next frame refused within 500 ms. 16. Update using another connection's clientID → refused; history author unchanged. 17. Update with missing dependencies repeated until the pending cap → disconnect, RSS flat. 18. Clock `0xFFFFFFFF`, struct length `2^53`, delete set over 2^32 range, unknown clients. 19. Truncated/garbage varints, 2 MiB + 1 frame, 10 000 tiny frames/min (rate limit). 20. Valid update that sets an element to an invalid schema value (NaN, 1e308) → rolled back, sender disconnected. 21. Awareness 1 MiB, spoofed user name, clock near max. 22. Move-storm for 10 minutes: history bytes and RSS bounded; quota charged to the owner; collaborator daily budget enforced. 23. Mass delete of 5 000 elements, then Restore. 24. Concurrent WebDAV PUT of the file during a live session → no lost edits, no 5xx. 25. Public link to a canvas: WebSocket with an Edit token → refused (unless §54 is changed). Cards and embeds: 26. Card pointing at `http://127.0.0.1`, `169.254.169.254`, `[::1]`, DNS-rebind host (reuse `dns_rebind_preload.c`), redirect to private IP → no fetch. 27. Preview HTML 50 MiB, slowloris server, `og:image` 100k x 100k PNG. 28. Viewer opens a canvas with external cards → zero outgoing requests from the server and from the browser to third-party hosts (network log). 29. `?el=` with `"]` `<img onerror>` and 10 KiB value → no injection, canvas opens. 30. SVG export of an element with `javascript:` link and an embeddable → output passes the allowlist; served with `sandbox` CSP. 31. Mermaid input of 10 MiB and a pathological diagram → Worker killed by budget, UI usable. ## 7. Isolation matrix cases (#472/#331) New routes to classify: canvas room WebSocket, canvas create/update/export API, card batch resolver, card live feed, URL preview fetch/proxy, image attachment upload/read, element-link resolution, history list/restore/undo. Cases (User A owner, User B recipient or stranger, anonymous public): - M1 B (no share) opens A's canvas by calternal-id, by `?el=`, via room WS, via export, via history → 404 identical to a missing ID. - M2 B with Share sees A's canvas; cards for A's Mail, Money, Contacts, Tasks, private Notes are placeholders; batch resolver returns uniform placeholders; timing/size match a missing ID. - M3 B with Collaborate adds a card for B's own Money item; A sees a placeholder. - M4 B places cards with A's item IDs on B's canvas → A's backlinks/Inspector show nothing; A gets no notice. - M5 Search as B (Share) for a word only in A's Money card title → no hit on the canvas. - M6 Public link `/s/<slug>`: every calternal card is a placeholder, no live feed, no WS write, no preview fetch triggered. - M7 Revoke Share during a live session → WS closed, card feed closed, placeholders. - M8 B's history view/per-author undo cannot reveal or revert another canvas, and undo of B's turn cannot remove A's edits made later. - M9 Preview proxy: B cannot read A's cached preview by URL hash or ID unless B can read a canvas that contains it. - M10 Image attachment fileId of A's canvas fetched by B directly → 404. - M11 Export by B (Share) embeds links only; resolved data is B's view only. - M12 MCP/CLI/WebMCP as B: same results as M1–M11 (one event path).
Author
Owner

Owner decisions 2026-10-03 (DESIGN §60, c7cf2bf8b + this commit): canvas element → calternal item. Make Task/Event/Note only on text, shapes with text and frames; batch with one Undo; Tasks to default list, Notes next to canvas, Events date from text or popover; back-links to the element. The drawn element KEEPS its look and is linked: corner status mark (checkbox/tick, date), text synced both ways (first line title, rest notes), click mark opens item, 'Show as card' swaps to ItemCard; copy keeps the link, 'Duplicate as new item' creates a new one. Arrows carry no meaning in v1. Include this in this issue's scope.

Owner decisions 2026-10-03 (DESIGN §60, c7cf2bf8b + this commit): canvas element → calternal item. Make Task/Event/Note only on text, shapes with text and frames; batch with one Undo; Tasks to default list, Notes next to canvas, Events date from text or popover; back-links to the element. The drawn element KEEPS its look and is linked: corner status mark (checkbox/tick, date), text synced both ways (first line title, rest notes), click mark opens item, 'Show as card' swaps to ItemCard; copy keeps the link, 'Duplicate as new item' creates a new one. Arrows carry no meaning in v1. Include this in this issue's scope.
Author
Owner

Owner C13 (2026-10-03, DESIGN §60): deleting a linked element keeps the item (toast offers 'Delete too'); item deleted elsewhere → element stays as plain drawing with a faint 'deleted' mark offering Undo or Remove link. In scope here.

Owner C13 (2026-10-03, DESIGN §60): deleting a linked element keeps the item (toast offers 'Delete <item> too'); item deleted elsewhere → element stays as plain drawing with a faint 'deleted' mark offering Undo or Remove link. In scope here.
Author
Owner

Starting work on branch job/canvas-cards-977. Branch base before the required origin/dev merge: e3915b5b79 (Canvas core #976). Merged origin/dev at 22f13995a9; current head is 4a9f0bc83f7bb3cb2e28c4206da3f2d86f18c2c3.

Starting work on branch job/canvas-cards-977. Branch base before the required origin/dev merge: e3915b5b79c39b6086db40d4c93f24c4289b36ed (Canvas core #976). Merged origin/dev at 22f13995a9c6020807af57018198ecfa3cf6b2f7; current head is 4a9f0bc83f7bb3cb2e28c4206da3f2d86f18c2c3.
Author
Owner

Correction to my start comment: the merge commit head SHA is a7b983f6ef (not 4a9f0bc83f7bb3cb2e28c4206da3f2d86f18c2c3). The branch base and origin/dev SHAs in that comment are correct.

Correction to my start comment: the merge commit head SHA is a7b983f6ef53d03ee42637da5a2e78b32b807fa6 (not 4a9f0bc83f7bb3cb2e28c4206da3f2d86f18c2c3). The branch base and origin/dev SHAs in that comment are correct.
Author
Owner

Finding (current head a7b983f6e): the #976 Canvas scaffold blocks drag/drop and paste in CanvasReact.tsx, and renderEmbeddable currently renders only the text "Item". The existing renderer already receives source embeddable elements and writes through sendCanvasElements. This checkout has a generic shared Card primitive, but no ItemCard export; the referenced #822 is titled "Calendar grid: thumbnails inside cards and vertical fan-out". I am checking the exact UI/runtime boundary before reusing its existing material.

Finding (current head a7b983f6e): the #976 Canvas scaffold blocks drag/drop and paste in CanvasReact.tsx, and renderEmbeddable currently renders only the text "Item". The existing renderer already receives source embeddable elements and writes through sendCanvasElements. This checkout has a generic shared Card primitive, but no ItemCard export; the referenced #822 is titled "Calendar grid: thumbnails inside cards and vertical fan-out". I am checking the exact UI/runtime boundary before reusing its existing material.
Author
Owner

Finding (#977): route-table review showed /p/{id} used the Photo detail API, whose response includes camera/location fields and has no safe display title. That made real Photo links resolve as unavailable cards. I changed the lookup to the existing permission-checked Files identity endpoint and project its entry name only, which avoids copying EXIF/location fields into the card. Added a route regression assertion for this mapping.

Finding (#977): route-table review showed `/p/{id}` used the Photo detail API, whose response includes camera/location fields and has no safe display title. That made real Photo links resolve as unavailable cards. I changed the lookup to the existing permission-checked Files identity endpoint and project its entry name only, which avoids copying EXIF/location fields into the card. Added a route regression assertion for this mapping.
Author
Owner

Finding (#977): apps/web/src/lib/files/transfer.ts writes internal Files drags as application/x-calternal-files JSON paths and puts only base names in text/plain. Canvas was reading only text/uri-list or text/plain as URLs, so dragging a Files row to a Canvas did nothing. I am consuming the existing Files drag MIME and resolving the temporary paths through the existing stat() API to stable /f/{item-id} links before inserting cards; the drop remains bounded to 20 items.

Finding (#977): `apps/web/src/lib/files/transfer.ts` writes internal Files drags as `application/x-calternal-files` JSON paths and puts only base names in `text/plain`. Canvas was reading only `text/uri-list` or `text/plain` as URLs, so dragging a Files row to a Canvas did nothing. I am consuming the existing Files drag MIME and resolving the temporary paths through the existing `stat()` API to stable `/f/{item-id}` links before inserting cards; the drop remains bounded to 20 items.
Author
Owner

Finding (#977): the first cargo test -p calternal-server run exposed that my new Mail privacy test inspected the raw authorized Plugin object and incorrectly expected its body to be absent. The actual API privacy boundary is the serialized ResolvedCard. I corrected the regression test to assert the real response contains the subject but not the private message body; the product projection already omits the body.

Finding (#977): the first `cargo test -p calternal-server` run exposed that my new Mail privacy test inspected the raw authorized Plugin object and incorrectly expected its body to be absent. The actual API privacy boundary is the serialized `ResolvedCard`. I corrected the regression test to assert the real response contains the subject but not the private message body; the product projection already omits the body.
Author
Owner

Finding (#977): PluginEvent already includes plugin_id and path, and the Canvas collaboration writer publishes a Notes update after its 250 ms save batch. The card SSE handler was treating every same-User event as an invalidation of every visible card. A User actively drawing could therefore trigger repeated resolver batches and hit the route's 30-batch/minute limit. I am sending only the trusted Plugin ID as the SSE invalidation and filtering it client-side to card links for that Plugin; the server also resolves the active Canvas identity once and suppresses its own Note path. Paths and item fields stay server-side.

Finding (#977): `PluginEvent` already includes `plugin_id` and `path`, and the Canvas collaboration writer publishes a Notes update after its 250 ms save batch. The card SSE handler was treating every same-User event as an invalidation of every visible card. A User actively drawing could therefore trigger repeated resolver batches and hit the route's 30-batch/minute limit. I am sending only the trusted Plugin ID as the SSE invalidation and filtering it client-side to card links for that Plugin; the server also resolves the active Canvas identity once and suppresses its own Note path. Paths and item fields stay server-side.
Author
Owner

#977 final report

READY FOR MERGE: no. This branch builds the viewer-scoped card foundation and a production Note/File slice. The issue still requires several user-facing parts listed below.

What I built

  • Added POST /api/v1/canvas/cards/resolve and GET /api/v1/canvas/cards/events. Resolution runs through the existing authenticated Plugin reads, returns a limited display projection, and uses one placeholder shape for malformed, deleted, unsupported or unreadable links. The stream sends Plugin invalidations without item data.
  • Added bounded requests: 200 links per batch, 512 KiB body, 2,048 bytes per link, 16 concurrent Plugin reads, and 30 batches per User per minute.
  • Added a shared viewer-scoped client cache, visible-card rendering, Plugin invalidation and refresh, stable-link normalization, card/link mode switching with Undo, open and Copy link actions, and retry/error states.
  • Added the shared search palette as the “Add item” picker. URI drops and pasted links create cards through the Canvas collaboration event path. File drops use the existing Files upload queue or resolve Files drag payloads to stable item IDs.
  • Added a production E2E path for Note and File cards, unavailable and external-link states, mode changes, Copy link, open, file upload/drop, and six macOS-platform screenshots.

Files

  • Server/contracts: crates/calternal-server/src/canvas_cards.rs, crates/calternal-server/src/main.rs, contracts/actions.json, contracts/action-overrides.json, contracts/openapi.json, packages/api-client/src/generated.ts, scripts/action_registry.py.
  • Web: apps/web/src/lib/canvas/CanvasReact.tsx, CanvasView.svelte, cards.ts, cards.test.ts, canvas.css, scene.ts, scene.test.ts, apps/web/src/lib/components/search-dialog.svelte, apps/web/src/lib/search/window.svelte.ts.
  • Verification/profile: apps/web/e2e/canvas-cards-977.mjs, bench/canvas-cards-977.mjs.
  • Lockfiles/manifests: apps/web/package.json, bun.lock, Cargo.lock.

Head and gates

Head SHA: a00f78bd81b723462890f7dafe9ea837af3e1790.

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

cargo clippy -p calternal-server --all-targets -- -D warnings output:

Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 20s

cargo test -p calternal-server result:

test result: ok. 170 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 13.35s

bun run check output:

User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Focused Vitest output:

 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web

 Test Files  3 passed (3)
      Tests  25 passed (25)
   Start at  02:06:28
   Duration  406ms (transform 63%, import 20%, tests 11%, worker 5%)

node --check e2e/canvas-cards-977.mjs and node --check ../../bench/canvas-cards-977.mjs: both exit 0, no output.

Production E2E output:

Canvas cards: URI drop, event persistence, viewer resolution, placeholder, mode, Copy link, open and six macOS screenshots passed.

The screenshots are in ignored artifacts/canvas-cards-977/: 390-light-card.png, 390-dark-card.png, 820-light-card.png, 820-dark-card.png, 1440-light-card.png, and 1440-dark-card.png. The installed fj issue comment command accepts text or --body-file but has no attachment option, so I could not upload the images; they remain in the worktree and were not committed.

Known gaps

  • External URL cards show a safe host label only. DESIGN §60 requires a server-fetched preview with an SSRF guard and same-origin image serving.
  • Backlinks and Inspector entries (“On ”) are not wired.
  • Contact and Note heading/block cards are not resolved. The issue asks for all listed item kinds and deep links; this branch does not yet provide or verify complete coverage.
  • The E2E does not yet exercise the requested Task completion elsewhere → live card update flow. Screenshots cover the Note slice, not every card kind.
  • The cross-User shared-Canvas private Mail/Money isolation matrix and full adversarial route probes were not run in this branch. Generic Canvas event actions already exist, but card add/remove parity through API, CLI, MCP and WebMCP was not independently verified.
  • The bench profile was added but not measured; the current verification policy reserves perf measurements for performance issues and the perf VM.

UX gaps closed

  • Cards have open and Copy link actions in read mode, mode switching in edit mode, Undo for mode changes, visible loading/error/unavailable states, and retry after a refresh error.
  • The shared picker supports selection without navigating away. Pointer/touch drops, paste, keyboard palette use, and file upload use the existing search, Files and collaboration paths.
  • Files drag payloads resolve to stable item IDs before entering Canvas source. Per-viewer cache state clears when the User or Canvas changes.

UX gaps left

  • Remaining item kinds, external preview content, backlinks, full shared-User isolation coverage, and the Task-update acceptance flow remain open as listed above.

Decisions where DESIGN did not set a value

  • Set resolver limits at 200 links per batch, 512 KiB per request, 2 KiB per link, 16 concurrent reads and 30 batches per User per minute, with a bounded 8,192-User limiter map. These caps bound request work and retained state.
  • Revalidate visible cards every 15 seconds in addition to Plugin invalidation events. Card display mode lives in the element’s customData.calternalCard.mode; source data stores only the stable link and mode.

For the merge round

  • tests/adversarial/run.sh against a real local server: add/run hostile payload, rate-limit, concurrency and cross-User cases for both new card endpoints; prove unreadable shared targets return the same placeholder and SSE exposes no item details.
  • cd apps/web && bun run test: run the full web suite on the combined branch.
  • Extend and run node apps/web/e2e/canvas-cards-977.mjs: cover Task completion from another view, remaining supported card kinds, and the full six-viewport/theme card screenshot matrix before marking #977 ready.
# #977 final report **READY FOR MERGE: no.** This branch builds the viewer-scoped card foundation and a production Note/File slice. The issue still requires several user-facing parts listed below. ## What I built - Added `POST /api/v1/canvas/cards/resolve` and `GET /api/v1/canvas/cards/events`. Resolution runs through the existing authenticated Plugin reads, returns a limited display projection, and uses one placeholder shape for malformed, deleted, unsupported or unreadable links. The stream sends Plugin invalidations without item data. - Added bounded requests: 200 links per batch, 512 KiB body, 2,048 bytes per link, 16 concurrent Plugin reads, and 30 batches per User per minute. - Added a shared viewer-scoped client cache, visible-card rendering, Plugin invalidation and refresh, stable-link normalization, card/link mode switching with Undo, open and Copy link actions, and retry/error states. - Added the shared search palette as the “Add item” picker. URI drops and pasted links create cards through the Canvas collaboration event path. File drops use the existing Files upload queue or resolve Files drag payloads to stable item IDs. - Added a production E2E path for Note and File cards, unavailable and external-link states, mode changes, Copy link, open, file upload/drop, and six macOS-platform screenshots. ## Files - Server/contracts: `crates/calternal-server/src/canvas_cards.rs`, `crates/calternal-server/src/main.rs`, `contracts/actions.json`, `contracts/action-overrides.json`, `contracts/openapi.json`, `packages/api-client/src/generated.ts`, `scripts/action_registry.py`. - Web: `apps/web/src/lib/canvas/CanvasReact.tsx`, `CanvasView.svelte`, `cards.ts`, `cards.test.ts`, `canvas.css`, `scene.ts`, `scene.test.ts`, `apps/web/src/lib/components/search-dialog.svelte`, `apps/web/src/lib/search/window.svelte.ts`. - Verification/profile: `apps/web/e2e/canvas-cards-977.mjs`, `bench/canvas-cards-977.mjs`. - Lockfiles/manifests: `apps/web/package.json`, `bun.lock`, `Cargo.lock`. ## Head and gates Head SHA: `a00f78bd81b723462890f7dafe9ea837af3e1790`. `cargo fmt --all --check`: exit 0, no output. `cargo clippy -p calternal-server --all-targets -- -D warnings` output: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 20s ``` `cargo test -p calternal-server` result: ``` test result: ok. 170 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 13.35s ``` `bun run check` output: ``` User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Focused Vitest output: ``` RUN v5.0.1 /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web Test Files 3 passed (3) Tests 25 passed (25) Start at 02:06:28 Duration 406ms (transform 63%, import 20%, tests 11%, worker 5%) ``` `node --check e2e/canvas-cards-977.mjs` and `node --check ../../bench/canvas-cards-977.mjs`: both exit 0, no output. Production E2E output: ``` Canvas cards: URI drop, event persistence, viewer resolution, placeholder, mode, Copy link, open and six macOS screenshots passed. ``` The screenshots are in ignored `artifacts/canvas-cards-977/`: `390-light-card.png`, `390-dark-card.png`, `820-light-card.png`, `820-dark-card.png`, `1440-light-card.png`, and `1440-dark-card.png`. The installed `fj issue comment` command accepts text or `--body-file` but has no attachment option, so I could not upload the images; they remain in the worktree and were not committed. ## Known gaps - External URL cards show a safe host label only. DESIGN §60 requires a server-fetched preview with an SSRF guard and same-origin image serving. - Backlinks and Inspector entries (“On <canvas name>”) are not wired. - Contact and Note heading/block cards are not resolved. The issue asks for all listed item kinds and deep links; this branch does not yet provide or verify complete coverage. - The E2E does not yet exercise the requested Task completion elsewhere → live card update flow. Screenshots cover the Note slice, not every card kind. - The cross-User shared-Canvas private Mail/Money isolation matrix and full adversarial route probes were not run in this branch. Generic Canvas event actions already exist, but card add/remove parity through API, CLI, MCP and WebMCP was not independently verified. - The bench profile was added but not measured; the current verification policy reserves perf measurements for performance issues and the perf VM. ## UX gaps closed - Cards have open and Copy link actions in read mode, mode switching in edit mode, Undo for mode changes, visible loading/error/unavailable states, and retry after a refresh error. - The shared picker supports selection without navigating away. Pointer/touch drops, paste, keyboard palette use, and file upload use the existing search, Files and collaboration paths. - Files drag payloads resolve to stable item IDs before entering Canvas source. Per-viewer cache state clears when the User or Canvas changes. ## UX gaps left - Remaining item kinds, external preview content, backlinks, full shared-User isolation coverage, and the Task-update acceptance flow remain open as listed above. ## Decisions where DESIGN did not set a value - Set resolver limits at 200 links per batch, 512 KiB per request, 2 KiB per link, 16 concurrent reads and 30 batches per User per minute, with a bounded 8,192-User limiter map. These caps bound request work and retained state. - Revalidate visible cards every 15 seconds in addition to Plugin invalidation events. Card display mode lives in the element’s `customData.calternalCard.mode`; source data stores only the stable link and mode. ## For the merge round - `tests/adversarial/run.sh` against a real local server: add/run hostile payload, rate-limit, concurrency and cross-User cases for both new card endpoints; prove unreadable shared targets return the same placeholder and SSE exposes no item details. - `cd apps/web && bun run test`: run the full web suite on the combined branch. - Extend and run `node apps/web/e2e/canvas-cards-977.mjs`: cover Task completion from another view, remaining supported card kinds, and the full six-viewport/theme card screenshot matrix before marking #977 ready.
Author
Owner

Round 2 started on job/canvas-cards-977, base/head a00f78bd8. Reading §60 and existing resolver/renderer. Round 1 SSE exists, but polling is unconditional. No ItemCard family is present in this checkout or fetched origin/dev; checking #822 dependency before integration. No pushes or deploys.

Round 2 started on job/canvas-cards-977, base/head a00f78bd8. Reading §60 and existing resolver/renderer. Round 1 SSE exists, but polling is unconditional. No ItemCard family is present in this checkout or fetched origin/dev; checking #822 dependency before integration. No pushes or deploys.
Author
Owner

Round 2 finding: ItemCard exists on origin/job/agenda-decks, not origin/dev. Reused that component and its VoiceWaveform dependency with public exports only; added glyph variants for Event, Money, Contact, Settings and saved search. Canvas renders this Svelte component without remounting on each live update. Commits: 0e7e0d815 (renderer), 5d08c866c (transport).

Transport tests prove healthy SSE stops polling, reconnect refreshes missed changes, continuous invalidations flush after 120 ms rather than sliding forever, and late stale responses cannot replace a newer projection. Focused Vitest: 2 files, 24 tests passed. bun run check: svelte-check found 0 errors and 0 warnings.

Coverage finding: this checkout has no Contacts plugin/API or Contact route in the §33 grammar. Providing actual Contact reads requires another plugin behavior/contract change, beyond a small public helper addition. Contact glyph support is present but is not claimed as complete Contact resolution.

Round 2 finding: ItemCard exists on origin/job/agenda-decks, not origin/dev. Reused that component and its VoiceWaveform dependency with public exports only; added glyph variants for Event, Money, Contact, Settings and saved search. Canvas renders this Svelte component without remounting on each live update. Commits: 0e7e0d815 (renderer), 5d08c866c (transport). Transport tests prove healthy SSE stops polling, reconnect refreshes missed changes, continuous invalidations flush after 120 ms rather than sliding forever, and late stale responses cannot replace a newer projection. Focused Vitest: 2 files, 24 tests passed. bun run check: svelte-check found 0 errors and 0 warnings. Coverage finding: this checkout has no Contacts plugin/API or Contact route in the §33 grammar. Providing actual Contact reads requires another plugin behavior/contract change, beyond a small public helper addition. Contact glyph support is present but is not claimed as complete Contact resolution.
Author
Owner

Round 2 progress: server gates pass (Clippy; 173 passed, 5 ignored). Focused Vitest: 2 files, 25 tests passed. The production test now proves Task completion through the real Task API updates the mounted Canvas card within five seconds, and captures twelve macOS-platform screenshots (Note and Task, 390/820/1440, light/dark). No existing assertion/status expectation was changed; action selectors now use the shared menu required by the owner.

Current head eff623a15. The next UI check corrects selection toolbar visibility: actions are visible on hover, focus or selection; touch keeps the menu trigger. Remaining scope is C11 conversion/two-way sync/duplicate, C13 deletion recovery, backlinks, Contact resolution, server-fetched external previews and event producers for Mail/Calendar/saved search.

The bench profile now creates 300 real cards, opens/pans them and changes 50 real Note targets. Measurement is deferred per the newer Verification policy (only perf issues measure on the perf VM). No performance figures are claimed.

Round 2 progress: server gates pass (Clippy; 173 passed, 5 ignored). Focused Vitest: 2 files, 25 tests passed. The production test now proves Task completion through the real Task API updates the mounted Canvas card within five seconds, and captures twelve macOS-platform screenshots (Note and Task, 390/820/1440, light/dark). No existing assertion/status expectation was changed; action selectors now use the shared menu required by the owner. Current head eff623a15. The next UI check corrects selection toolbar visibility: actions are visible on hover, focus or selection; touch keeps the menu trigger. Remaining scope is C11 conversion/two-way sync/duplicate, C13 deletion recovery, backlinks, Contact resolution, server-fetched external previews and event producers for Mail/Calendar/saved search. The bench profile now creates 300 real cards, opens/pans them and changes 50 real Note targets. Measurement is deferred per the newer Verification policy (only perf issues measure on the perf VM). No performance figures are claimed.
Author
Owner

#977 round 2 report

READY FOR MERGE: no. The renderer, resolver and live-update slice passes its focused checks. C11, C13, backlinks and complete item-kind coverage remain unfinished.

What I built

  • Reused the ItemCard from origin/job/agenda-decks (#822), including its VoiceWaveform dependency. Added public ItemCard/ItemChip exports and glyph variants for Event, Money, Contact, Settings and saved search. These are small shared UI additions; no existing shared component behavior changed. No dependencies were added.
  • Mounted the shared Svelte content inside the React Canvas island. A store updates its projection without remounting the card. CSS container queries choose density from the element's actual space. Cards mount only in view.
  • Removed permanent text action buttons. Copy link and card/link mode actions use the shared menu and a toolbar on hover, focus or selection. Touch keeps the menu trigger. Keyboard menus anchor to the control. Warm tooltips remain.
  • Removed the extra drawn card outline from the renderer. Imported paint stays in the source and is restored before element events and exports.
  • Fixed live batching: a fixed 120 ms window cannot slide forward forever under a storm; late reads cannot overwrite a newer projection. Healthy Notes/Files feeds stop polling. Reconnect refreshes missed changes. Mail, Calendar and saved search retain scoped polling because their writes do not publish on the Plugin bus in this checkout.
  • Added Note heading/block selection after the authorized Note read, canonical Money transaction routing, File-backed photo/folder classification, and same-origin authorized photo thumbnail URLs. Missing anchors return the uniform placeholder. Generated the API contract and client.
  • Extended the bench profile to persist 300 real cards, open and pan the Canvas, and change 50 real Note targets while measuring the live render.
  • Added a real Task completion regression with a five-second deadline, below the polling interval. The production flow also verifies URI/File drops, persistence, placeholders, open, Copy link, mode Undo and keyboard menu activation.

Files

  • apps/web/e2e/canvas-cards-977.mjs
  • apps/web/src/lib/canvas/CanvasItemCard.svelte
  • apps/web/src/lib/canvas/CanvasReact.tsx
  • apps/web/src/lib/canvas/canvas.css
  • apps/web/src/lib/canvas/cards.test.ts
  • apps/web/src/lib/canvas/cards.ts
  • bench/canvas-cards-977.mjs
  • contracts/openapi.json
  • crates/calternal-server/src/canvas_cards.rs
  • packages/api-client/src/generated.ts
  • packages/ui/src/components/ItemCard.svelte
  • packages/ui/src/components/composer/VoiceWaveform.svelte
  • packages/ui/src/index.ts

Head and gates

Head: 0afa84cd4bc9b6caf9be3f1b88005dd488e7cc07 on job/canvas-cards-977. The one-time fetch and merge of origin/dev reported Already up to date. No push, deploy or branch merge was performed after that required catch-up.

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

cargo clippy -p calternal-server --all-targets -- -D warnings (verbatim completion):

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 08s

cargo test -p calternal-server (verbatim result):

test result: ok. 173 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 21.12s

bun run check (verbatim):

$ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bunx vitest run src/lib/canvas/cards.test.ts src/lib/canvas/scene.test.ts --maxWorkers=2 (verbatim):


 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web


 Test Files  2 passed (2)
      Tests  25 passed (25)
   Start at  02:45:30
   Duration  496ms (transform 63%, tests 19%, import 15%, worker 3%)

Production build completed. Focused production E2E (verbatim):

Canvas cards: URI drop, event persistence, viewer resolution, placeholder, mode, Copy link, open, Task completion over SSE and twelve macOS screenshots passed.

Command: CALTERNAL_E2E_ASSET_OVERRIDE=1 CALTERNAL_SERVER_BIN=<job debug binary> node apps/web/e2e/canvas-cards-977.mjs. CURRENT for the shared server was absent, so this job built its server with the preset target directory and the required Cargo environment. E2E grants the local-network and clipboard permissions used by Chromium on this host. No existing status/assertion expectation was changed; selectors now exercise the owner-required menu.

node --check apps/web/e2e/canvas-cards-977.mjs, node --check bench/canvas-cards-977.mjs and git diff --check: exit 0, no output.

Production evidence

Twelve macOS-platform captures are attached to this issue. They cover Notes and completed Tasks at 390/820/1440 in light and dark. Artifacts were not committed. The orchestrator reviews visual quality.

UX gaps closed

  • Card actions no longer occupy permanent text rows. Pointer, keyboard focus, selection and touch menu access use the shared controls.
  • Card content uses the shared ItemCard family and one material border. Imported source paint is preserved.
  • Note anchors show the selected content, not a false match on the parent title.
  • Photo/folder links retain their kind; photo images use the Instance origin.
  • A Task completed outside the Canvas changes its card through SSE within the focused five-second deadline. Storm batching and stale-response tests pass.

Known gaps / UX gaps left

  • C11 is not built: Make Task/Event/Note from text, text-in-shape and frames; corner marks; two-way title/notes sync; batch Undo; Show as card; Duplicate as new item.
  • C13 is not built: delete-element toast offering item deletion, and deleted-item drawing marks with Undo/Remove link.
  • “On ” backlinks and Inspector entries are not wired.
  • Contact resolution is absent: this checkout has no Contact read API and §33 has no Contact link grammar. Adding a Contact subsystem is more than a small public helper addition.
  • External cards still show the host only. Server-fetched previews, the SSRF guard path and cached same-origin preview images are not built.
  • Mail/Calendar/saved-search event producers are absent. Those kinds still use polling as a fallback; complete live coverage and shared-recipient invalidation/revocation need work.
  • Production evidence covers Note and Task, plus File behavior. It does not cover every requested kind, thumbnails, or a converted sticky.
  • The 300-card/50-update profile is extended and syntax-checked, but not measured. No performance figures are claimed. There is no Canvas metric in docs/perf/baseline.json.
  • Full cross-User card isolation, public-share cases, export parity and the adversarial matrices remain for the merge round.

Decisions

  • Reuse the existing #822 branch component because it is not yet on origin/dev; use a Svelte host rather than a second React implementation.
  • Use CSS container queries for available-space density. Do not measure text or layout at runtime.
  • Poll only missing event producers while SSE is healthy; poll visible links during transport loss. This preserves existing updates until the missing producers are supplied.
  • Keep optional thumbnail URLs out of placeholders. Note anchors reuse the existing Markdown parser and slug grammar; missing anchors do not fall back to the parent Note.
  • Defer measurement under the newer Verification policy: this is a feature issue, and performance runs are reserved for performance issues on the perf VM. This policy supersedes the brief's measurement request.

For the merge round

  • Complete the listed scope before marking #977 ready, then extend the production capture matrix to every kind and a converted sticky.
  • cd apps/web && bun run test: the full web suite on the combined branch.
  • CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-cards-977.mjs with CALTERNAL_SERVER_BIN set: run the focused acceptance flow on the combined production build.
  • tests/adversarial/run.sh: one time-boxed real-server round, including the card routes and shared-Canvas private Mail/Money isolation. The combined branch must prove uniform placeholders and no item data in invalidations.
  • On the perf VM, with the shared release binary in CALTERNAL_SERVER_BIN: flock /root/perf.lock node bench/canvas-cards-977.mjs --json artifacts/canvas-cards-977-profile.json. The script records load inside the lock. Establish the Canvas baseline and report resolver/open latency, CPU/RSS, pan and the 50-item live storm.

Module comments were read again before this report. Cargo output and web build output were removed after verification; the screenshots and logs remain in ignored artifacts/canvas-cards-977/.

# #977 round 2 report **READY FOR MERGE: no.** The renderer, resolver and live-update slice passes its focused checks. C11, C13, backlinks and complete item-kind coverage remain unfinished. ## What I built - Reused the ItemCard from `origin/job/agenda-decks` (#822), including its VoiceWaveform dependency. Added public ItemCard/ItemChip exports and glyph variants for Event, Money, Contact, Settings and saved search. These are small shared UI additions; no existing shared component behavior changed. No dependencies were added. - Mounted the shared Svelte content inside the React Canvas island. A store updates its projection without remounting the card. CSS container queries choose density from the element's actual space. Cards mount only in view. - Removed permanent text action buttons. Copy link and card/link mode actions use the shared menu and a toolbar on hover, focus or selection. Touch keeps the menu trigger. Keyboard menus anchor to the control. Warm tooltips remain. - Removed the extra drawn card outline from the renderer. Imported paint stays in the source and is restored before element events and exports. - Fixed live batching: a fixed 120 ms window cannot slide forward forever under a storm; late reads cannot overwrite a newer projection. Healthy Notes/Files feeds stop polling. Reconnect refreshes missed changes. Mail, Calendar and saved search retain scoped polling because their writes do not publish on the Plugin bus in this checkout. - Added Note heading/block selection after the authorized Note read, canonical Money transaction routing, File-backed photo/folder classification, and same-origin authorized photo thumbnail URLs. Missing anchors return the uniform placeholder. Generated the API contract and client. - Extended the bench profile to persist 300 real cards, open and pan the Canvas, and change 50 real Note targets while measuring the live render. - Added a real Task completion regression with a five-second deadline, below the polling interval. The production flow also verifies URI/File drops, persistence, placeholders, open, Copy link, mode Undo and keyboard menu activation. ## Files - `apps/web/e2e/canvas-cards-977.mjs` - `apps/web/src/lib/canvas/CanvasItemCard.svelte` - `apps/web/src/lib/canvas/CanvasReact.tsx` - `apps/web/src/lib/canvas/canvas.css` - `apps/web/src/lib/canvas/cards.test.ts` - `apps/web/src/lib/canvas/cards.ts` - `bench/canvas-cards-977.mjs` - `contracts/openapi.json` - `crates/calternal-server/src/canvas_cards.rs` - `packages/api-client/src/generated.ts` - `packages/ui/src/components/ItemCard.svelte` - `packages/ui/src/components/composer/VoiceWaveform.svelte` - `packages/ui/src/index.ts` ## Head and gates Head: `0afa84cd4bc9b6caf9be3f1b88005dd488e7cc07` on `job/canvas-cards-977`. The one-time fetch and merge of `origin/dev` reported `Already up to date.` No push, deploy or branch merge was performed after that required catch-up. `cargo fmt --check`: exit 0, no output. `cargo clippy -p calternal-server --all-targets -- -D warnings` (verbatim completion): ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 08s ``` `cargo test -p calternal-server` (verbatim result): ``` test result: ok. 173 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 21.12s ``` `bun run check` (verbatim): ``` $ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bunx vitest run src/lib/canvas/cards.test.ts src/lib/canvas/scene.test.ts --maxWorkers=2` (verbatim): ``` RUN v5.0.1 /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web Test Files 2 passed (2) Tests 25 passed (25) Start at 02:45:30 Duration 496ms (transform 63%, tests 19%, import 15%, worker 3%) ``` Production build completed. Focused production E2E (verbatim): ``` Canvas cards: URI drop, event persistence, viewer resolution, placeholder, mode, Copy link, open, Task completion over SSE and twelve macOS screenshots passed. ``` Command: `CALTERNAL_E2E_ASSET_OVERRIDE=1 CALTERNAL_SERVER_BIN=<job debug binary> node apps/web/e2e/canvas-cards-977.mjs`. CURRENT for the shared server was absent, so this job built its server with the preset target directory and the required Cargo environment. E2E grants the local-network and clipboard permissions used by Chromium on this host. No existing status/assertion expectation was changed; selectors now exercise the owner-required menu. `node --check apps/web/e2e/canvas-cards-977.mjs`, `node --check bench/canvas-cards-977.mjs` and `git diff --check`: exit 0, no output. ## Production evidence Twelve macOS-platform captures are attached to this issue. They cover Notes and completed Tasks at 390/820/1440 in light and dark. Artifacts were not committed. The orchestrator reviews visual quality. - [390-light-card.png](https://git.kayg.org/attachments/575e3a15-fa43-4452-a808-055a68b87609) - [390-dark-card.png](https://git.kayg.org/attachments/c243b841-affe-4b08-b428-e96b322226bf) - [820-light-card.png](https://git.kayg.org/attachments/51dd4b6a-7778-40bb-a250-02e4c7a6a21c) - [820-dark-card.png](https://git.kayg.org/attachments/fda48915-d903-4780-8449-5a58a4781c73) - [1440-light-card.png](https://git.kayg.org/attachments/51669ef7-101f-4bf9-9934-781d348c690c) - [1440-dark-card.png](https://git.kayg.org/attachments/6b8cb710-72bb-4776-b0de-86b3ac21a750) - [390-light-task.png](https://git.kayg.org/attachments/c9303a73-e4e6-4780-9855-1621fc58567a) - [390-dark-task.png](https://git.kayg.org/attachments/18a2698c-4d05-48ca-9bd2-66b3dfabf160) - [820-light-task.png](https://git.kayg.org/attachments/0dcb8ce7-7c0b-417f-890c-8b284b53a5ae) - [820-dark-task.png](https://git.kayg.org/attachments/e57d672d-449f-4d25-9513-d813c9717866) - [1440-light-task.png](https://git.kayg.org/attachments/1dcd1000-a209-47c8-802f-40d7b8c3c830) - [1440-dark-task.png](https://git.kayg.org/attachments/caa22e2d-5480-4267-8e38-6c400e966774) ## UX gaps closed - Card actions no longer occupy permanent text rows. Pointer, keyboard focus, selection and touch menu access use the shared controls. - Card content uses the shared ItemCard family and one material border. Imported source paint is preserved. - Note anchors show the selected content, not a false match on the parent title. - Photo/folder links retain their kind; photo images use the Instance origin. - A Task completed outside the Canvas changes its card through SSE within the focused five-second deadline. Storm batching and stale-response tests pass. ## Known gaps / UX gaps left - C11 is not built: Make Task/Event/Note from text, text-in-shape and frames; corner marks; two-way title/notes sync; batch Undo; Show as card; Duplicate as new item. - C13 is not built: delete-element toast offering item deletion, and deleted-item drawing marks with Undo/Remove link. - “On <canvas>” backlinks and Inspector entries are not wired. - Contact resolution is absent: this checkout has no Contact read API and §33 has no Contact link grammar. Adding a Contact subsystem is more than a small public helper addition. - External cards still show the host only. Server-fetched previews, the SSRF guard path and cached same-origin preview images are not built. - Mail/Calendar/saved-search event producers are absent. Those kinds still use polling as a fallback; complete live coverage and shared-recipient invalidation/revocation need work. - Production evidence covers Note and Task, plus File behavior. It does not cover every requested kind, thumbnails, or a converted sticky. - The 300-card/50-update profile is extended and syntax-checked, but not measured. No performance figures are claimed. There is no Canvas metric in `docs/perf/baseline.json`. - Full cross-User card isolation, public-share cases, export parity and the adversarial matrices remain for the merge round. ## Decisions - Reuse the existing #822 branch component because it is not yet on `origin/dev`; use a Svelte host rather than a second React implementation. - Use CSS container queries for available-space density. Do not measure text or layout at runtime. - Poll only missing event producers while SSE is healthy; poll visible links during transport loss. This preserves existing updates until the missing producers are supplied. - Keep optional thumbnail URLs out of placeholders. Note anchors reuse the existing Markdown parser and slug grammar; missing anchors do not fall back to the parent Note. - Defer measurement under the newer Verification policy: this is a feature issue, and performance runs are reserved for performance issues on the perf VM. This policy supersedes the brief's measurement request. ## For the merge round - Complete the listed scope before marking #977 ready, then extend the production capture matrix to every kind and a converted sticky. - `cd apps/web && bun run test`: the full web suite on the combined branch. - `CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-cards-977.mjs` with `CALTERNAL_SERVER_BIN` set: run the focused acceptance flow on the combined production build. - `tests/adversarial/run.sh`: one time-boxed real-server round, including the card routes and shared-Canvas private Mail/Money isolation. The combined branch must prove uniform placeholders and no item data in invalidations. - On the perf VM, with the shared release binary in `CALTERNAL_SERVER_BIN`: `flock /root/perf.lock node bench/canvas-cards-977.mjs --json artifacts/canvas-cards-977-profile.json`. The script records load inside the lock. Establish the Canvas baseline and report resolver/open latency, CPU/RSS, pan and the 50-item live storm. Module comments were read again before this report. Cargo output and web build output were removed after verification; the screenshots and logs remain in ignored `artifacts/canvas-cards-977/`.
Author
Owner

Round 3 started on job/canvas-cards-977 at 0afa84cd4b. Read CLAUDE.md, CONTEXT.md and DESIGN §33, §34, §60. Continuing C11 first, reusing existing checked item writers and the Canvas collaboration event path. No new dependencies planned. The current verification policy defers perf measurements and full adversarial matrices to the merge round. No push or deployment.

Round 3 started on job/canvas-cards-977 at 0afa84cd4bc9b6caf9be3f1b88005dd488e7cc07. Read CLAUDE.md, CONTEXT.md and DESIGN §33, §34, §60. Continuing C11 first, reusing existing checked item writers and the Canvas collaboration event path. No new dependencies planned. The current verification policy defers perf measurements and full adversarial matrices to the merge round. No push or deployment.
Author
Owner

Round 3 finding: conversion planning is committed at b310a9832 (7 focused regressions). Creation reuses Notes POST, standalone Tasks POST, and Calendar POST. A small public createEventItem helper returns the existing CalendarEventDetail instead of discarding it; createEvent keeps its prior void contract. Notes use the Canvas folder; every created item includes its stable element backlink in real notes.

Recovery contract boundary: Notes DELETE and Files Trash accept If-Match. Calendar DELETE does not accept a caller-supplied revision; implementing full Event conversion Undo safely would alter another Plugin behavior/contract. Current conversion recovery detaches the drawing and retains the Event with a plain-language message. This is an explicit remaining C11 gap, not claimed as full Undo. No Contact read API or Contact deep-link grammar exists here; adding Contacts is another Plugin design change.

The required one-time git fetch origin and git merge origin/dev reported Already up to date. Shared-server CURRENT is absent; building this branch server for production screenshots with CARGO_BUILD_JOBS=4 and the preset target directory. No Rust source changed.

Round 3 finding: conversion planning is committed at b310a9832 (7 focused regressions). Creation reuses Notes POST, standalone Tasks POST, and Calendar POST. A small public createEventItem helper returns the existing CalendarEventDetail instead of discarding it; createEvent keeps its prior void contract. Notes use the Canvas folder; every created item includes its stable element backlink in real notes. Recovery contract boundary: Notes DELETE and Files Trash accept If-Match. Calendar DELETE does not accept a caller-supplied revision; implementing full Event conversion Undo safely would alter another Plugin behavior/contract. Current conversion recovery detaches the drawing and retains the Event with a plain-language message. This is an explicit remaining C11 gap, not claimed as full Undo. No Contact read API or Contact deep-link grammar exists here; adding Contacts is another Plugin design change. The required one-time git fetch origin and git merge origin/dev reported Already up to date. Shared-server CURRENT is absent; building this branch server for production screenshots with CARGO_BUILD_JOBS=4 and the preset target directory. No Rust source changed.
Author
Owner

#977 round 3 report

READY FOR MERGE: no. This round adds drawn-item creation and Note/Task deletion recovery. The full §60 contract still needs the work listed below.

What I built

  • Make Task / Make Event / Make Note from text and text-in-shape. Frames make Notes with their name as title and their text as a list. Selected bound labels and frame members do not create duplicate items. Planning scans labels once, so large selections do not repeat a whole-scene search for each shape.
  • Creation uses existing Plugin writers. Tasks stay standalone; Notes go beside the Canvas. The full first line remains the Task title after the Composer parser extracts properties. Further lines go into the item's notes. Each created item also gets a stable element backlink in its notes, labelled with the current Canvas title.
  • The drawing keeps its geometry, paint, text and font, with a visible corner status control. Task completion changes the mark through the existing card resolver and SSE. Marks mount only in view and use scene coordinates, with no DOM layout measurement.
  • Batch creation has one recovery toast. A failed batch keeps its successful item handles. Undo detaches the links and removes untouched Notes/Tasks through checked writers. It keeps items whose revision changed. This is not an atomic cross-Plugin transaction.
  • Show as card uses the existing ItemCard renderer. It hides a shape's bound label in the same scene update. Undo refuses newer drawing revisions. Remove link keeps both contents and has Undo. Corner menus use the shared Menu, warm tooltips and 44px targets; the phone menu fits to the left of the mark.
  • Drawing deletion keeps its linked Note/Task. The toast offers Delete Note too / Delete Task too. That action uses Files Trash with If-Match; Undo restores the real Trash identity. Missing or unreadable targets use a faint generic unavailable mark with Remove link, with no target-kind leak.
  • The small public createEventItem helper returns the Calendar writer's existing durable identity. createEvent retains its previous void contract and serializer. No dependencies or Rust source changed.
  • Extended the existing 300-card/50-item storm profile with 20 linked drawings that reuse real targets. Added a focused production conversion/recovery flow and reused the existing scene test helper.

Files

  • apps/web/e2e/canvas-cards-977.mjs
  • apps/web/e2e/canvas-conversion-977.mjs
  • apps/web/e2e/canvas-export-fixture.mjs
  • apps/web/src/lib/calendar/data.ts
  • apps/web/src/lib/canvas/CanvasItemCard.svelte
  • apps/web/src/lib/canvas/CanvasReact.tsx
  • apps/web/src/lib/canvas/CanvasView.svelte
  • apps/web/src/lib/canvas/canvas.css
  • apps/web/src/lib/canvas/conversion.test.ts
  • apps/web/src/lib/canvas/conversion.ts
  • apps/web/src/lib/canvas/conversionApi.test.ts
  • apps/web/src/lib/canvas/conversionApi.ts
  • bench/canvas-cards-977.mjs

Head and gates

Head: cccfbb41afb920a3c5626a627ee740e80259b1e5 on job/canvas-cards-977. Nine atomic commits were made in this round. The required one-time fetch and merge of origin/dev reported Already up to date. No push or deployment was performed.

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

bun run check (verbatim):

$ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bunx vitest run src/lib/canvas/conversion.test.ts src/lib/canvas/conversionApi.test.ts src/lib/canvas/cards.test.ts src/lib/canvas/scene.test.ts src/lib/calendar/data.test.ts --maxWorkers=2 (verbatim):

RUN  v5.0.1 /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web


 Test Files  5 passed (5)
      Tests  68 passed (68)
   Start at  03:31:27
   Duration  5.84s (transform 86%, import 10%, tests 3%)

Production build: cd apps/web && bun run build, exit 0. The existing exporter worker warnings remain in the build log.

Focused production checks (verbatim):

Canvas conversion: Task/Note creation, live status, conversion/card/link Undo, deletion recovery and six macOS screenshots passed.
Canvas cards: URI drop, event persistence, viewer resolution, placeholder, mode, Copy link, open, Task completion over SSE and twelve macOS screenshots passed.

Commands: CALTERNAL_E2E_ASSET_OVERRIDE=1 CALTERNAL_SERVER_BIN=<job debug binary> node apps/web/e2e/canvas-conversion-977.mjs and the same environment for node apps/web/e2e/canvas-cards-977.mjs. The shared-server CURRENT file was absent, so this job built the unchanged server after the web build, with the preset target directory and required Cargo variables.

Rust Clippy/tests were not run: no Rust crate changed, per the current verification policy. node --check passed for both card/conversion scripts, the shared scene fixture and the bench profile. git diff --check passed. Existing test assertions and status expectations were not changed; only the scene polling helper moved to the existing fixture module.

Production evidence

The six converted-sticky captures are attached to this issue. They emulate macOS and cover 390/820/1440, light and dark. The original card flow also recaptured its twelve Note/Task images in the worktree. No review artifacts were committed. The orchestrator reviews visual quality.

UX gaps closed

  • A converted sticky keeps its drawing and real notes. Its mark opens the item and shows live Task completion within the focused five-second deadline.
  • Text and shape conversion, frame planning, literal Task titles and partial creation recovery have focused regressions. The real production flow proves Task and Note creation, Note conversion Undo, card Undo, Remove link Undo, drawing deletion keeping its Task, optional Task deletion, and restoration of the same Task identity.
  • Corner menus work with pointer, keyboard focus and touch controls. The phone menu no longer extends past the viewport. Source edits and newer link associations are checked before linking or undoing.
  • Event creation asks for a date/calendar and has real loading, failure and no-calendar states.

Known gaps / UX gaps left

  1. C11 remains partial: title/notes are copied at creation, not kept in two-way sync. Duplicate as new item is absent. Native drawing copy/paste link retention is not verified; the existing safe paste boundary still blocks drawing payloads. Events use a calendar glyph rather than a date mark. Multi-item Undo is implemented and unit-tested; full batch and frame interaction need production coverage.
  2. Full Event conversion Undo is blocked by the Calendar delete contract: it has no caller-supplied revision condition. This round keeps the Event on Undo and says so. Changing that contract is more than a small public helper addition in another Plugin.
  3. C13 covers Note/Task element deletion and Remove link. Other kinds have no deletion action here. Item deletion elsewhere does not offer restore Undo; deleted and unreadable targets still share the generic unavailable mark.
  4. The stable element backlink is written into new item notes, but the required “On ” incoming backlinks and Inspector rows are not wired for all item kinds.
  5. Contact resolution remains absent. This checkout has no Contact read API or Contact deep-link grammar. A Contact subsystem needs another Plugin design change.
  6. External link cards still show a host label. Server preview fetching through the SSRF guard and same-origin preview images are not built.
  7. No event producers were added. The inherited fallback inventory is unchanged: Mail, Calendar/Event and saved-search links poll while SSE is healthy; all visible links poll during transport loss. Complete producer and shared-recipient coverage remains open.
  8. Screenshots now include the converted sticky, Note and Task flows. Every remaining requested item kind and a real connected-calendar Event conversion still need production evidence. Full cross-User/private Mail/Money isolation remains for the merge round.
  9. The profile was extended and syntax-checked, not measured. The current verification policy explicitly defers performance runs for feature briefs to the merge round. No numbers are claimed; docs/perf/baseline.json has no Canvas card metric.

Decisions

  • Use existing writers and preserve partial success rather than add a new cross-Plugin transaction route. An old file revision cannot remove a later edit.
  • Create all-day Events. A real ISO date in the first line seeds the date field; otherwise the User chooses it. The first visible writable-calendar candidate seeds the calendar selector. Relative date parsing and timed Event inference are not added.
  • Keep Events during conversion Undo until the Calendar writer supports checked deletion. This is a known contract gap, not full Event Undo.
  • Store only the item kind marker and stable link in drawing metadata. The corner status is a viewer-specific projection from the existing card store.
  • Use a separate focused production server/User for conversions. Combining all screenshot navigation with the earlier suite exhausted its existing per-User resolver budget. The focused suite now stays within that budget; the limit was not changed.
  • Keep performance measurement and the full adversarial matrices for the merge round, as the verification policy requires.

For the merge round

  • Complete the remaining scope before marking #977 ready.
  • cd apps/web && bun run test: full web checks on the combined branch.
  • With the shared server binary in CALTERNAL_SERVER_BIN: CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-cards-977.mjs and CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-conversion-977.mjs: preserve existing cards and conversion/recovery behavior in the combined production build.
  • tests/adversarial/run.sh: one time-boxed real-server round, including card isolation, public/share reads and revocation. Prove uniform placeholders and no item data in invalidations.
  • On the perf VM, with its shared release binary in CALTERNAL_SERVER_BIN: flock /root/perf.lock node bench/canvas-cards-977.mjs --json artifacts/canvas-cards-977-profile.json. The profile records load inside the lock. Report p50/p95, CPU/RSS, 300 cards, 20 drawing marks and the 50-target storm next to the baseline.

Module and function comments were read again and updated before reporting. Cargo clean removed 4.6 GiB; generated web build/output directories were removed. Screenshots, attachments and gate logs remain in ignored artifacts directories. The worktree is clean.

# #977 round 3 report **READY FOR MERGE: no.** This round adds drawn-item creation and Note/Task deletion recovery. The full §60 contract still needs the work listed below. ## What I built - Make Task / Make Event / Make Note from text and text-in-shape. Frames make Notes with their name as title and their text as a list. Selected bound labels and frame members do not create duplicate items. Planning scans labels once, so large selections do not repeat a whole-scene search for each shape. - Creation uses existing Plugin writers. Tasks stay standalone; Notes go beside the Canvas. The full first line remains the Task title after the Composer parser extracts properties. Further lines go into the item's notes. Each created item also gets a stable element backlink in its notes, labelled with the current Canvas title. - The drawing keeps its geometry, paint, text and font, with a visible corner status control. Task completion changes the mark through the existing card resolver and SSE. Marks mount only in view and use scene coordinates, with no DOM layout measurement. - Batch creation has one recovery toast. A failed batch keeps its successful item handles. Undo detaches the links and removes untouched Notes/Tasks through checked writers. It keeps items whose revision changed. This is not an atomic cross-Plugin transaction. - Show as card uses the existing ItemCard renderer. It hides a shape's bound label in the same scene update. Undo refuses newer drawing revisions. Remove link keeps both contents and has Undo. Corner menus use the shared Menu, warm tooltips and 44px targets; the phone menu fits to the left of the mark. - Drawing deletion keeps its linked Note/Task. The toast offers Delete Note too / Delete Task too. That action uses Files Trash with If-Match; Undo restores the real Trash identity. Missing or unreadable targets use a faint generic unavailable mark with Remove link, with no target-kind leak. - The small public createEventItem helper returns the Calendar writer's existing durable identity. createEvent retains its previous void contract and serializer. No dependencies or Rust source changed. - Extended the existing 300-card/50-item storm profile with 20 linked drawings that reuse real targets. Added a focused production conversion/recovery flow and reused the existing scene test helper. ## Files - `apps/web/e2e/canvas-cards-977.mjs` - `apps/web/e2e/canvas-conversion-977.mjs` - `apps/web/e2e/canvas-export-fixture.mjs` - `apps/web/src/lib/calendar/data.ts` - `apps/web/src/lib/canvas/CanvasItemCard.svelte` - `apps/web/src/lib/canvas/CanvasReact.tsx` - `apps/web/src/lib/canvas/CanvasView.svelte` - `apps/web/src/lib/canvas/canvas.css` - `apps/web/src/lib/canvas/conversion.test.ts` - `apps/web/src/lib/canvas/conversion.ts` - `apps/web/src/lib/canvas/conversionApi.test.ts` - `apps/web/src/lib/canvas/conversionApi.ts` - `bench/canvas-cards-977.mjs` ## Head and gates Head: `cccfbb41afb920a3c5626a627ee740e80259b1e5` on `job/canvas-cards-977`. Nine atomic commits were made in this round. The required one-time fetch and merge of origin/dev reported `Already up to date.` No push or deployment was performed. `cargo fmt --check`: exit 0, no output. `bun run check` (verbatim): ``` $ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bunx vitest run src/lib/canvas/conversion.test.ts src/lib/canvas/conversionApi.test.ts src/lib/canvas/cards.test.ts src/lib/canvas/scene.test.ts src/lib/calendar/data.test.ts --maxWorkers=2` (verbatim): ``` RUN v5.0.1 /home/kayg/Developer/calternal-wt/canvas-cards-977/apps/web Test Files 5 passed (5) Tests 68 passed (68) Start at 03:31:27 Duration 5.84s (transform 86%, import 10%, tests 3%) ``` Production build: `cd apps/web && bun run build`, exit 0. The existing exporter worker warnings remain in the build log. Focused production checks (verbatim): ``` Canvas conversion: Task/Note creation, live status, conversion/card/link Undo, deletion recovery and six macOS screenshots passed. Canvas cards: URI drop, event persistence, viewer resolution, placeholder, mode, Copy link, open, Task completion over SSE and twelve macOS screenshots passed. ``` Commands: `CALTERNAL_E2E_ASSET_OVERRIDE=1 CALTERNAL_SERVER_BIN=<job debug binary> node apps/web/e2e/canvas-conversion-977.mjs` and the same environment for `node apps/web/e2e/canvas-cards-977.mjs`. The shared-server CURRENT file was absent, so this job built the unchanged server after the web build, with the preset target directory and required Cargo variables. Rust Clippy/tests were not run: no Rust crate changed, per the current verification policy. `node --check` passed for both card/conversion scripts, the shared scene fixture and the bench profile. `git diff --check` passed. Existing test assertions and status expectations were not changed; only the scene polling helper moved to the existing fixture module. ## Production evidence The six converted-sticky captures are attached to this issue. They emulate macOS and cover 390/820/1440, light and dark. The original card flow also recaptured its twelve Note/Task images in the worktree. No review artifacts were committed. The orchestrator reviews visual quality. - [1440-dark-converted-sticky.png](https://git.kayg.org/attachments/11fdaa39-0df1-43a3-ab5c-b1f5bfd9fdba) - [1440-light-converted-sticky.png](https://git.kayg.org/attachments/161511b7-aa09-4ff4-beff-8b317323c84b) - [390-dark-converted-sticky.png](https://git.kayg.org/attachments/22259bca-e403-43c4-b841-424066adb090) - [390-light-converted-sticky.png](https://git.kayg.org/attachments/60d61850-5e6e-444c-a87e-fd5d05d05e6d) - [820-dark-converted-sticky.png](https://git.kayg.org/attachments/46903121-7bef-48a7-a75b-921b1fc4cdb4) - [820-light-converted-sticky.png](https://git.kayg.org/attachments/8279adb9-e6b2-48be-9873-8797879bda58) ## UX gaps closed - A converted sticky keeps its drawing and real notes. Its mark opens the item and shows live Task completion within the focused five-second deadline. - Text and shape conversion, frame planning, literal Task titles and partial creation recovery have focused regressions. The real production flow proves Task and Note creation, Note conversion Undo, card Undo, Remove link Undo, drawing deletion keeping its Task, optional Task deletion, and restoration of the same Task identity. - Corner menus work with pointer, keyboard focus and touch controls. The phone menu no longer extends past the viewport. Source edits and newer link associations are checked before linking or undoing. - Event creation asks for a date/calendar and has real loading, failure and no-calendar states. ## Known gaps / UX gaps left 1. C11 remains partial: title/notes are copied at creation, not kept in two-way sync. Duplicate as new item is absent. Native drawing copy/paste link retention is not verified; the existing safe paste boundary still blocks drawing payloads. Events use a calendar glyph rather than a date mark. Multi-item Undo is implemented and unit-tested; full batch and frame interaction need production coverage. 2. Full Event conversion Undo is blocked by the Calendar delete contract: it has no caller-supplied revision condition. This round keeps the Event on Undo and says so. Changing that contract is more than a small public helper addition in another Plugin. 3. C13 covers Note/Task element deletion and Remove link. Other kinds have no deletion action here. Item deletion elsewhere does not offer restore Undo; deleted and unreadable targets still share the generic unavailable mark. 4. The stable element backlink is written into new item notes, but the required “On <canvas>” incoming backlinks and Inspector rows are not wired for all item kinds. 5. Contact resolution remains absent. This checkout has no Contact read API or Contact deep-link grammar. A Contact subsystem needs another Plugin design change. 6. External link cards still show a host label. Server preview fetching through the SSRF guard and same-origin preview images are not built. 7. No event producers were added. The inherited fallback inventory is unchanged: Mail, Calendar/Event and saved-search links poll while SSE is healthy; all visible links poll during transport loss. Complete producer and shared-recipient coverage remains open. 8. Screenshots now include the converted sticky, Note and Task flows. Every remaining requested item kind and a real connected-calendar Event conversion still need production evidence. Full cross-User/private Mail/Money isolation remains for the merge round. 9. The profile was extended and syntax-checked, not measured. The current verification policy explicitly defers performance runs for feature briefs to the merge round. No numbers are claimed; docs/perf/baseline.json has no Canvas card metric. ## Decisions - Use existing writers and preserve partial success rather than add a new cross-Plugin transaction route. An old file revision cannot remove a later edit. - Create all-day Events. A real ISO date in the first line seeds the date field; otherwise the User chooses it. The first visible writable-calendar candidate seeds the calendar selector. Relative date parsing and timed Event inference are not added. - Keep Events during conversion Undo until the Calendar writer supports checked deletion. This is a known contract gap, not full Event Undo. - Store only the item kind marker and stable link in drawing metadata. The corner status is a viewer-specific projection from the existing card store. - Use a separate focused production server/User for conversions. Combining all screenshot navigation with the earlier suite exhausted its existing per-User resolver budget. The focused suite now stays within that budget; the limit was not changed. - Keep performance measurement and the full adversarial matrices for the merge round, as the verification policy requires. ## For the merge round - Complete the remaining scope before marking #977 ready. - `cd apps/web && bun run test`: full web checks on the combined branch. - With the shared server binary in CALTERNAL_SERVER_BIN: `CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-cards-977.mjs` and `CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-conversion-977.mjs`: preserve existing cards and conversion/recovery behavior in the combined production build. - `tests/adversarial/run.sh`: one time-boxed real-server round, including card isolation, public/share reads and revocation. Prove uniform placeholders and no item data in invalidations. - On the perf VM, with its shared release binary in CALTERNAL_SERVER_BIN: `flock /root/perf.lock node bench/canvas-cards-977.mjs --json artifacts/canvas-cards-977-profile.json`. The profile records load inside the lock. Report p50/p95, CPU/RSS, 300 cards, 20 drawing marks and the 50-target storm next to the baseline. Module and function comments were read again and updated before reporting. Cargo clean removed 4.6 GiB; generated web build/output directories were removed. Screenshots, attachments and gate logs remain in ignored artifacts directories. The worktree is clean.
Author
Owner

Round 4 continuation started on job/canvas-cards-977 at cccfbb41af. Priorities: conflict-safe two-way drawing text sync through the existing event path, then Canvas backlinks. No pushes or deploys. Verification follows the latest per-crate and focused-web policy.

Round 4 continuation started on job/canvas-cards-977 at cccfbb41afb920a3c5626a627ee740e80259b1e5. Priorities: conflict-safe two-way drawing text sync through the existing event path, then Canvas backlinks. No pushes or deploys. Verification follows the latest per-crate and focused-web policy.
Author
Owner

Finding: the live room saves Canvas files as the Canvas owner (calternal-collab/src/session.rs flush_canvas_locked). A collaborator can edit the Canvas without read/write access to its linked items. Two-way item writes must authorize the originating editor per linked item; an owner-scoped flush callback would grant unintended access. No such callback added. Backlinks now reuse the existing note_links index with stable item keys, cover Notes plus Home File/Photo/Task identities, and remove tombstoned drawings. Focused regressions cover rename, deduplication and cross-User isolation. Decisions: one membership row per Canvas; anchors refer to the same item, and membership opens the stable Canvas Note link.

Finding: the live room saves Canvas files as the Canvas owner (calternal-collab/src/session.rs flush_canvas_locked). A collaborator can edit the Canvas without read/write access to its linked items. Two-way item writes must authorize the originating editor per linked item; an owner-scoped flush callback would grant unintended access. No such callback added. Backlinks now reuse the existing note_links index with stable item keys, cover Notes plus Home File/Photo/Task identities, and remove tombstoned drawings. Focused regressions cover rename, deduplication and cross-User isolation. Decisions: one membership row per Canvas; anchors refer to the same item, and membership opens the stable Canvas Note link.
Author
Owner

Round 4 continuation report. READY FOR MERGE: no.

Head: 019f0947ba955347f5563367e013fb038a9f965a. Base: cccfbb41afb920a3c5626a627ee740e80259b1e5. Branch: job/canvas-cards-977. Six atomic commits. The required git fetch origin && git merge origin/dev ran once before final gates and returned Already up to date.

Built

  • Canvas membership uses stable item keys in the existing note_links Index. It covers own Note, Task, File, folder and Photo identities, including Note heading/block links. Deleted drawings create no membership. A target rename preserves the link. Several drawings for one item produce one Canvas row.
  • Notes mentions and the shared Files Inspector content show “On ”, open the stable Canvas Note identity and offer Copy link. Canvas mentions do not read scene JSON for excerpts. Inspector rows refresh from the existing Notes stream; stale responses cannot restore removed rows.
  • Added a focused production regression. It checks real Canvas event writes, stable navigation, clipboard content and live membership removal/restoration in an open Inspector. Captured both surfaces at 390/820/1440, light and dark, with macOS platform emulation.
  • Extended the existing bench profile with backlink p50/p95, CPU/RSS sampling and a 300-request burst across its existing real targets. No measurement claimed.

Files

apps/web/e2e/canvas-backlinks-977.mjs
apps/web/src/lib/files/InfoPanel.svelte
apps/web/src/lib/notes/mentions.ts
apps/web/src/lib/notes/notes.test.ts
bench/canvas-cards-977.mjs
crates/plugins/notes/src/lib.rs
crates/plugins/notes/src/store.rs
packages/ui/src/components/notes/NoteMentionsCard.svelte

Gates (verbatim output)

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

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 12s

cargo test -p calternal-plugin-notes -- --test-threads=4:

test result: ok. 194 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 119.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.25s
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:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 15s

cargo test -p calternal-server -- --test-threads=4:

test result: ok. 173 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 12.99s

cd apps/web && bun run check:

svelte-check found 0 errors and 0 warnings

cd apps/web && bunx vitest run src/lib/notes/notes.test.ts --maxWorkers=2:

 Test Files  1 passed (1)
      Tests  10 passed (10)

Production build: cd apps/web && bun run build, exit 0. Existing exporter worker warnings remain.

Focused production command: CALTERNAL_SERVER_BIN=<job debug binary> CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-backlinks-977.mjs:

Canvas backlinks: real membership, stable navigation, Copy link, live removal/restoration and twelve macOS screenshots passed.

node --check passed for the focused regression and bench profile. git diff --check passed. Existing test expectations were kept. Added tests cover target rename, deduplication, tombstones, standalone Task/File/Photo identity lookup and cross-User isolation. Module and function comments were read again and updated before this report.

UX gaps closed

  • Incoming Canvas membership now appears in Notes mentions and the shared Files Inspector content.
  • Open Inspector membership updates after Canvas events. Stable navigation and Copy link work at every required width/theme.
  • Membership labels and Copy link controls align on one line. Links have a 44 px minimum target. The twelve production captures are attached; visual review remains with the orchestrator.

Known gaps / UX gaps left

  • Two-way text sync is not built. The live room flush runs as the Canvas owner. A collaborator can edit the Canvas without access to its linked private items. Item writes must authorize the originating editor per linked item and retain a conditional text baseline. An owner-scoped flush callback would grant unintended access. This needs changes to the collaboration/item-write behavior beyond a small public helper in another crate; I stopped at that boundary under the job's ownership rule.
  • Backlinks are not wired into every Plugin-specific Inspector. Shared/public recipient Canvas membership, Events, Mail, Money, Contacts, saved searches and other non-Home kinds remain open. Direct Tasks/Calendar popover and Photos-specific screenshot coverage are not claimed.
  • Duplicate as new item, full Event/deletion Undo, Contact cards, server-fetched external preview cards, same-origin preview images and the remaining feed producers are unchanged from the previous report.
  • Touch targets and accessible anchors are present; this capture run uses pointer and keyboard opening in macOS-emulated Chromium, not a separate real touch or Apple-client run.
  • No perf VM measurement ran. The current verification policy limits performance runs to performance issues and leaves this feature profile for the merge round. docs/perf/baseline.json has no Canvas card metric.

Decisions

  • Reuse the link Index instead of adding a membership table or write route. Stable keys are derived data and are never used to build a filesystem path.
  • Show one membership row per Canvas. Heading/block links belong to the same item. A membership row opens the full Canvas by stable Note identity; conversion notes still retain their exact element backlink.
  • Keep the existing backlink response shape; kind: "canvas" selects its membership label and Copy link action.

Evidence

For the merge round

  • Complete the remaining #977 scope before marking it ready.
  • cd apps/web && bun run test: run the full web suite on the combined branch.
  • CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-backlinks-977.mjs with the combined production build and its server in CALTERNAL_SERVER_BIN: preserve membership, copy/navigation and live Inspector updates.
  • Run the inherited card/conversion flows: node apps/web/e2e/canvas-cards-977.mjs and node apps/web/e2e/canvas-conversion-977.mjs with the same environment.
  • tests/adversarial/run.sh: one time-boxed merge-round check, including private Mail/Money card placeholders, share/public reads and revocation. The branch job did not run the full matrix, per current policy.
  • On the perf VM with the shared release server and this production web build: flock /root/perf.lock node bench/canvas-cards-977.mjs --json artifacts/canvas-cards-977-profile.json. Load is recorded by the profile inside the lock. Report resolver/backlink p50/p95, CPU/RSS, rendering, the 50-target update storm and bursts beside the baseline.

Cleanup completed: Cargo clean removed 8.9 GiB. Web build/output directories were removed. Screenshots and logs remain in ignored artifacts. The worktree is clean. No push, deploy, issue closure or merge into dev/main occurred.

Round 4 continuation report. READY FOR MERGE: **no**. Head: `019f0947ba955347f5563367e013fb038a9f965a`. Base: `cccfbb41afb920a3c5626a627ee740e80259b1e5`. Branch: `job/canvas-cards-977`. Six atomic commits. The required `git fetch origin && git merge origin/dev` ran once before final gates and returned `Already up to date.` ## Built - Canvas membership uses stable item keys in the existing `note_links` Index. It covers own Note, Task, File, folder and Photo identities, including Note heading/block links. Deleted drawings create no membership. A target rename preserves the link. Several drawings for one item produce one Canvas row. - Notes mentions and the shared Files Inspector content show “On <canvas>”, open the stable Canvas Note identity and offer Copy link. Canvas mentions do not read scene JSON for excerpts. Inspector rows refresh from the existing Notes stream; stale responses cannot restore removed rows. - Added a focused production regression. It checks real Canvas event writes, stable navigation, clipboard content and live membership removal/restoration in an open Inspector. Captured both surfaces at 390/820/1440, light and dark, with macOS platform emulation. - Extended the existing bench profile with backlink p50/p95, CPU/RSS sampling and a 300-request burst across its existing real targets. No measurement claimed. ## Files ```text apps/web/e2e/canvas-backlinks-977.mjs apps/web/src/lib/files/InfoPanel.svelte apps/web/src/lib/notes/mentions.ts apps/web/src/lib/notes/notes.test.ts bench/canvas-cards-977.mjs crates/plugins/notes/src/lib.rs crates/plugins/notes/src/store.rs packages/ui/src/components/notes/NoteMentionsCard.svelte ``` ## Gates (verbatim output) `cargo fmt --check`: exit 0, no output. `cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 12s ``` `cargo test -p calternal-plugin-notes -- --test-threads=4`: ```text test result: ok. 194 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 119.03s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.25s 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`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 15s ``` `cargo test -p calternal-server -- --test-threads=4`: ```text test result: ok. 173 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 12.99s ``` `cd apps/web && bun run check`: ```text svelte-check found 0 errors and 0 warnings ``` `cd apps/web && bunx vitest run src/lib/notes/notes.test.ts --maxWorkers=2`: ```text Test Files 1 passed (1) Tests 10 passed (10) ``` Production build: `cd apps/web && bun run build`, exit 0. Existing exporter worker warnings remain. Focused production command: `CALTERNAL_SERVER_BIN=<job debug binary> CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-backlinks-977.mjs`: ```text Canvas backlinks: real membership, stable navigation, Copy link, live removal/restoration and twelve macOS screenshots passed. ``` `node --check` passed for the focused regression and bench profile. `git diff --check` passed. Existing test expectations were kept. Added tests cover target rename, deduplication, tombstones, standalone Task/File/Photo identity lookup and cross-User isolation. Module and function comments were read again and updated before this report. ## UX gaps closed - Incoming Canvas membership now appears in Notes mentions and the shared Files Inspector content. - Open Inspector membership updates after Canvas events. Stable navigation and Copy link work at every required width/theme. - Membership labels and Copy link controls align on one line. Links have a 44 px minimum target. The twelve production captures are attached; visual review remains with the orchestrator. ## Known gaps / UX gaps left - Two-way text sync is not built. The live room flush runs as the Canvas owner. A collaborator can edit the Canvas without access to its linked private items. Item writes must authorize the originating editor per linked item and retain a conditional text baseline. An owner-scoped flush callback would grant unintended access. This needs changes to the collaboration/item-write behavior beyond a small public helper in another crate; I stopped at that boundary under the job's ownership rule. - Backlinks are not wired into every Plugin-specific Inspector. Shared/public recipient Canvas membership, Events, Mail, Money, Contacts, saved searches and other non-Home kinds remain open. Direct Tasks/Calendar popover and Photos-specific screenshot coverage are not claimed. - Duplicate as new item, full Event/deletion Undo, Contact cards, server-fetched external preview cards, same-origin preview images and the remaining feed producers are unchanged from the previous report. - Touch targets and accessible anchors are present; this capture run uses pointer and keyboard opening in macOS-emulated Chromium, not a separate real touch or Apple-client run. - No perf VM measurement ran. The current verification policy limits performance runs to performance issues and leaves this feature profile for the merge round. `docs/perf/baseline.json` has no Canvas card metric. ## Decisions - Reuse the link Index instead of adding a membership table or write route. Stable keys are derived data and are never used to build a filesystem path. - Show one membership row per Canvas. Heading/block links belong to the same item. A membership row opens the full Canvas by stable Note identity; conversion notes still retain their exact element backlink. - Keep the existing backlink response shape; `kind: "canvas"` selects its membership label and Copy link action. ## Evidence - [1440-dark-inspector.png](https://git.kayg.org/attachments/e58daf70-550d-42fa-bc98-2d50ed2bc88d) - [1440-dark-mentions.png](https://git.kayg.org/attachments/d45b9833-c3bc-4696-bd07-b449b50c4ed4) - [1440-light-inspector.png](https://git.kayg.org/attachments/3c6f4988-6f63-4c68-abd3-48e5c4fa1ced) - [1440-light-mentions.png](https://git.kayg.org/attachments/1594ae5e-257d-400b-ac85-381b1431f78c) - [390-dark-inspector.png](https://git.kayg.org/attachments/0ac46bd3-3628-46a6-80ec-0afb05775ffd) - [390-dark-mentions.png](https://git.kayg.org/attachments/3617b002-b7e6-4756-ae4f-b9a061f97e37) - [390-light-inspector.png](https://git.kayg.org/attachments/e256d049-1c6c-4ba3-9478-e710910cd2b9) - [390-light-mentions.png](https://git.kayg.org/attachments/30c90926-003f-48b3-a49c-453c14173944) - [820-dark-inspector.png](https://git.kayg.org/attachments/0ca98706-eb25-4bde-9d6d-4a495b4da0c0) - [820-dark-mentions.png](https://git.kayg.org/attachments/69919886-f2a9-44cc-bd13-f5a3117b29a8) - [820-light-inspector.png](https://git.kayg.org/attachments/10cf386a-cd27-4673-9d25-fef7fb6fa5b6) - [820-light-mentions.png](https://git.kayg.org/attachments/d90f4f27-b393-4332-a2ff-d713459b0e40) ## For the merge round - Complete the remaining #977 scope before marking it ready. - `cd apps/web && bun run test`: run the full web suite on the combined branch. - `CALTERNAL_E2E_ASSET_OVERRIDE=1 node apps/web/e2e/canvas-backlinks-977.mjs` with the combined production build and its server in `CALTERNAL_SERVER_BIN`: preserve membership, copy/navigation and live Inspector updates. - Run the inherited card/conversion flows: `node apps/web/e2e/canvas-cards-977.mjs` and `node apps/web/e2e/canvas-conversion-977.mjs` with the same environment. - `tests/adversarial/run.sh`: one time-boxed merge-round check, including private Mail/Money card placeholders, share/public reads and revocation. The branch job did not run the full matrix, per current policy. - On the perf VM with the shared release server and this production web build: `flock /root/perf.lock node bench/canvas-cards-977.mjs --json artifacts/canvas-cards-977-profile.json`. Load is recorded by the profile inside the lock. Report resolver/backlink p50/p95, CPU/RSS, rendering, the 50-target update storm and bursts beside the baseline. Cleanup completed: Cargo clean removed 8.9 GiB. Web build/output directories were removed. Screenshots and logs remain in ignored artifacts. The worktree is clean. No push, deploy, issue closure or merge into dev/main occurred.
Author
Owner

Round 5 started on job/canvas-cards-977 at 019f0947ba955347f5563367e013fb038a9f965a. Implementing originating-editor authorization for linked drawing text at the shared Canvas event boundary, with conditional item revisions. Canvas owner-scoped saves will not authorize linked-item writes. Then Duplicate as new item and checked Event Undo as time allows. Per-crate/focused verification; no push or deploy.

Round 5 started on `job/canvas-cards-977` at `019f0947ba955347f5563367e013fb038a9f965a`. Implementing originating-editor authorization for linked drawing text at the shared Canvas event boundary, with conditional item revisions. Canvas owner-scoped saves will not authorize linked-item writes. Then Duplicate as new item and checked Event Undo as time allows. Per-crate/focused verification; no push or deploy.
Author
Owner

Round 5 finding: linked Note/Task text can use the existing Notes lock and checked replace. The event adapter now validates a scratch scene before an item write, and supplies the authenticated editor to the Files share check. Owner, view-only, Collaborate, revocation between read/write and stale-baseline tests passed. A real event regression also proves that an invalid sibling element cannot partially write its item.

Decisions: one linked item text change per event, because cross-Home item batches have no atomic writer; ordinary drawing/geometry batches keep their existing contract. A read-only item projection may refresh a drawing only when its text equals the authorized current item. Visible drawing subscriptions request at most 20 source-text projections per resolver batch, capped at 64 KiB each; ordinary cards keep small labels. Duplicate as new item copies and detaches the drawing before calling the existing conversion writer. Event/Contact/external-preview completion is not claimed yet.

Round 5 finding: linked Note/Task text can use the existing Notes lock and checked replace. The event adapter now validates a scratch scene before an item write, and supplies the authenticated editor to the Files share check. Owner, view-only, Collaborate, revocation between read/write and stale-baseline tests passed. A real event regression also proves that an invalid sibling element cannot partially write its item. Decisions: one linked item text change per event, because cross-Home item batches have no atomic writer; ordinary drawing/geometry batches keep their existing contract. A read-only item projection may refresh a drawing only when its text equals the authorized current item. Visible drawing subscriptions request at most 20 source-text projections per resolver batch, capped at 64 KiB each; ordinary cards keep small labels. Duplicate as new item copies and detaches the drawing before calling the existing conversion writer. Event/Contact/external-preview completion is not claimed yet.
Author
Owner

#977 round 5 report

READY FOR MERGE: no

Branch: job/canvas-cards-977
Base: 019f0947ba955347f5563367e013fb038a9f965a
Head: 637a6834b53ec9e3b5eb9d13a106628b7340ba9c

Built

  • Note and standalone Task text sync uses the authenticated editing User on the shared HTTP/WebSocket Canvas event path. Canvas owner scope cannot supply item write authority.
  • Item ownership permits edits. Other Users need a current item-bound Files edit grant. Reads, final writes and live card refreshes check current access. A checked item write preserves workflow state, properties and the generated element backlink.
  • Server preflight validates the complete Canvas event before it writes an item. Stale item text and stale identities fail closed. A concurrent item change remains intact.
  • Bounded source projections update linked text live. Read-only text has a plain owner hint. Geometry remains editable. Item share changes invalidate card access without polling.
  • Duplicate as new item copies the drawing with fresh element IDs and creates a distinct item through the existing conversion writer. Normal copy keeps its link.
  • Shared Task marks retain workflow status. Open and Copy link use the authorized stable Files identity for a shared item.
  • Tests cover owner, absent grant, view grant, edit grant, revocation during a write and on an open WebSocket, concurrent item edits, invalid sibling events, live refresh and distinct duplication.
  • Extended the benchmark with checked durable text saves and a concurrent event burst. No new dependency or migration was needed.

Files

apps/web/e2e/canvas-text-977.mjs
apps/web/src/lib/canvas/CanvasItemCard.svelte
apps/web/src/lib/canvas/CanvasReact.tsx
apps/web/src/lib/canvas/CanvasView.svelte
apps/web/src/lib/canvas/cards.test.ts
apps/web/src/lib/canvas/cards.ts
apps/web/src/lib/canvas/conversion.test.ts
apps/web/src/lib/canvas/conversion.ts
apps/web/src/lib/canvas/linkedText.test.ts
apps/web/src/lib/canvas/linkedText.ts
bench/canvas-cards-977.mjs
contracts/openapi.json
crates/calternal-collab/src/canvas.rs
crates/calternal-collab/src/session.rs
crates/calternal-server/src/canvas_cards.rs
crates/calternal-server/src/main.rs
crates/calternal-server/src/wire.rs
crates/plugins/notes/src/canvas_text.rs
crates/plugins/notes/src/lib.rs
packages/api-client/src/generated.ts

UX gaps closed

  • A recipient could open a text editor that could never save. Pointer and keyboard guards now stop it and expose the owner hint.
  • Clearing editor state after revocation left the upstream textarea writable. The guard now restores its authoritative value and closes it through the upstream blur handler.
  • The generic warm tooltip adapter replaced the owner hint. ItemCard controls now retain their own guidance.
  • Shared item Open used an own-Home route. It now uses the stable shared reader; the focused production test checks keyboard Open.
  • Shared Task cards lost their workflow status. The source projection now includes that state.
  • Healthy linked text refreshes used a timer. Shared item and share invalidations now drive them.

Known gaps / UX gaps left

  • Event text sync and Event Undo are unfinished. The existing Event keep action remains because checked deletion needs an item revision contract.
  • Frame-to-Note text detection exists, but item-to-frame text projection is unfinished. Do not treat frame text sync as complete.
  • A Canvas event can change the text of one linked item. Cross-item text batches fail closed. Item and Canvas persistence have no atomic cross-file transaction; dedicated shared Undo and conflict recovery remain incomplete.
  • Conflict rejection preserves the newer item, but the UI uses the current generic reconnect/error behavior rather than a dedicated conflict action.
  • Contact cards and external link previews through the SSRF guard remain unfinished. This round adds no external fetch path.
  • The actual shared Canvas UI flow is not complete. Shared-author authorization is tested through its real WebSocket, and the recipient-owned Canvas demonstrates shared-item read-only guidance.
  • Notes text updates retain the current file name. The title changes; this round does not move the source file.
  • Full suites, the adversarial matrices, real macOS client checks and performance measurement remain for the merge round.

Decisions

  • Only whole Notes and standalone Tasks use this text writer. Unsupported item kinds fail closed.
  • Recheck the item grant after the baseline read. Grant projections in the browser are guidance, never authority.
  • Reject multi-item text events until an atomic cross-Home writer exists. Validate all sibling elements before the first item write.
  • Keep the Note path during a text-only edit to avoid an implicit cross-file move transaction.
  • Request source text in batches of at most 20 items. Ordinary card batches still permit 200 links. Each source projection is smaller than 64 KiB.
  • Do not replace active or pending local edits with a late source projection. The server performs the baseline conflict check.

Verification

The required single fetch and merge of origin/dev returned Already up to date. No push, deployment, or merge into dev/main was done. The working tree is clean. Reviewed the changed doc comments and ran git diff --check and both new JavaScript syntax checks.

Gate output below is verbatim. cargo fmt --check exited 0 with no output. Per-crate commands used CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=4 and the worktree temporary directory. The full collab suite ran before the geometry fast-path refinement; its focused event regression and clippy passed after that refinement.

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 30.88s
test result: ok. 196 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 97.92s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.36s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.89s
test result: ok. 45 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.27s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.58s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 33.56s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.46s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.81s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.85s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.38s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.56s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.07s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.39s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Collab focused event regression after fast-path change

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 44 filtered out; finished in 4.33s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 11 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 15 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s

Server: cargo clippy -p calternal-server --all-targets -- -D warnings; cargo test -p calternal-server

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 28.46s
test result: ok. 173 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 23.66s

Web: bun run check; bunx vitest run src/lib/canvas/linkedText.test.ts src/lib/canvas/cards.test.ts src/lib/canvas/conversion.test.ts src/lib/canvas/conversionApi.test.ts --maxWorkers=2

svelte-check found 0 errors and 0 warnings
 Test Files  4 passed (4)
      Tests  33 passed (33)

The production web build exited 0. It retains the existing exporter worker import.meta warnings. The focused production run used the job server and its real production web build:

CALTERNAL_SERVER_BIN=$CARGO_TARGET_DIR/debug/calternal-server CALTERNAL_E2E_ASSET_OVERRIDE=1 TMPDIR=$PWD/target/tmp node apps/web/e2e/canvas-text-977.mjs
Canvas text: owner, Collaborate, Share, no item grant, revocation, concurrent edit, live refresh, duplicate identity pointer readonly guard, keyboard shared-item Open and twelve macOS captures passed.

Screenshots

Twelve production captures emulate macOS: 390, 820 and 1440 px, light and dark, for read-only guidance and duplication. These are evidence for the orchestrator to review. No review artifact is committed.

Performance

No performance numbers are claimed. The later verification policy reserves performance runs for performance issues and the merge round. The extended profile is syntax checked. docs/perf/baseline.json has no Canvas card baseline to compare here.

For the merge round

  • cd apps/web && bun run test -- --maxWorkers=2: run the combined web suite.
  • cd apps/web && bun run test:e2e: run the full shell suite with the combined production build.
  • bash tests/adversarial/run.sh: run the route, cross-User, authorization and robustness probes against the combined server. Check the new optional card fields and shared invalidations.
  • With the shared release server and production web assets on the perf VM: flock /root/perf.lock node bench/canvas-cards-977.mjs --json artifacts/canvas-cards-977-profile.json. The profile records load inside the lock. Record p50/p95, CPU and RSS; establish the Canvas baseline and check the burst. Never compile on that VM.
  • Review all twelve attached captures and check real macOS shared-item opening when the combined build is ready.

Cleanup

cargo clean output:

     Removed 18161 files, 11.8GiB total

Deleted the web production build and Svelte output. Kept logs and screenshots in ignored artifacts/.

# #977 round 5 report READY FOR MERGE: no Branch: `job/canvas-cards-977` Base: `019f0947ba955347f5563367e013fb038a9f965a` Head: `637a6834b53ec9e3b5eb9d13a106628b7340ba9c` Built - Note and standalone Task text sync uses the authenticated editing User on the shared HTTP/WebSocket Canvas event path. Canvas owner scope cannot supply item write authority. - Item ownership permits edits. Other Users need a current item-bound Files edit grant. Reads, final writes and live card refreshes check current access. A checked item write preserves workflow state, properties and the generated element backlink. - Server preflight validates the complete Canvas event before it writes an item. Stale item text and stale identities fail closed. A concurrent item change remains intact. - Bounded source projections update linked text live. Read-only text has a plain owner hint. Geometry remains editable. Item share changes invalidate card access without polling. - Duplicate as new item copies the drawing with fresh element IDs and creates a distinct item through the existing conversion writer. Normal copy keeps its link. - Shared Task marks retain workflow status. Open and Copy link use the authorized stable Files identity for a shared item. - Tests cover owner, absent grant, view grant, edit grant, revocation during a write and on an open WebSocket, concurrent item edits, invalid sibling events, live refresh and distinct duplication. - Extended the benchmark with checked durable text saves and a concurrent event burst. No new dependency or migration was needed. Files ``` apps/web/e2e/canvas-text-977.mjs apps/web/src/lib/canvas/CanvasItemCard.svelte apps/web/src/lib/canvas/CanvasReact.tsx apps/web/src/lib/canvas/CanvasView.svelte apps/web/src/lib/canvas/cards.test.ts apps/web/src/lib/canvas/cards.ts apps/web/src/lib/canvas/conversion.test.ts apps/web/src/lib/canvas/conversion.ts apps/web/src/lib/canvas/linkedText.test.ts apps/web/src/lib/canvas/linkedText.ts bench/canvas-cards-977.mjs contracts/openapi.json crates/calternal-collab/src/canvas.rs crates/calternal-collab/src/session.rs crates/calternal-server/src/canvas_cards.rs crates/calternal-server/src/main.rs crates/calternal-server/src/wire.rs crates/plugins/notes/src/canvas_text.rs crates/plugins/notes/src/lib.rs packages/api-client/src/generated.ts ``` UX gaps closed - A recipient could open a text editor that could never save. Pointer and keyboard guards now stop it and expose the owner hint. - Clearing editor state after revocation left the upstream textarea writable. The guard now restores its authoritative value and closes it through the upstream blur handler. - The generic warm tooltip adapter replaced the owner hint. ItemCard controls now retain their own guidance. - Shared item Open used an own-Home route. It now uses the stable shared reader; the focused production test checks keyboard Open. - Shared Task cards lost their workflow status. The source projection now includes that state. - Healthy linked text refreshes used a timer. Shared item and share invalidations now drive them. Known gaps / UX gaps left - Event text sync and Event Undo are unfinished. The existing Event keep action remains because checked deletion needs an item revision contract. - Frame-to-Note text detection exists, but item-to-frame text projection is unfinished. Do not treat frame text sync as complete. - A Canvas event can change the text of one linked item. Cross-item text batches fail closed. Item and Canvas persistence have no atomic cross-file transaction; dedicated shared Undo and conflict recovery remain incomplete. - Conflict rejection preserves the newer item, but the UI uses the current generic reconnect/error behavior rather than a dedicated conflict action. - Contact cards and external link previews through the SSRF guard remain unfinished. This round adds no external fetch path. - The actual shared Canvas UI flow is not complete. Shared-author authorization is tested through its real WebSocket, and the recipient-owned Canvas demonstrates shared-item read-only guidance. - Notes text updates retain the current file name. The title changes; this round does not move the source file. - Full suites, the adversarial matrices, real macOS client checks and performance measurement remain for the merge round. Decisions - Only whole Notes and standalone Tasks use this text writer. Unsupported item kinds fail closed. - Recheck the item grant after the baseline read. Grant projections in the browser are guidance, never authority. - Reject multi-item text events until an atomic cross-Home writer exists. Validate all sibling elements before the first item write. - Keep the Note path during a text-only edit to avoid an implicit cross-file move transaction. - Request source text in batches of at most 20 items. Ordinary card batches still permit 200 links. Each source projection is smaller than 64 KiB. - Do not replace active or pending local edits with a late source projection. The server performs the baseline conflict check. Verification The required single fetch and merge of `origin/dev` returned `Already up to date.` No push, deployment, or merge into dev/main was done. The working tree is clean. Reviewed the changed doc comments and ran `git diff --check` and both new JavaScript syntax checks. Gate output below is verbatim. `cargo fmt --check` exited 0 with no output. Per-crate commands used `CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=4` and the worktree temporary directory. The full collab suite ran before the geometry fast-path refinement; its focused event regression and clippy passed after that refinement. Notes: `cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings`; `cargo test -p calternal-plugin-notes` ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 30.88s test result: ok. 196 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 97.92s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.36s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` Collab: `cargo clippy -p calternal-collab --all-targets -- -D warnings`; `cargo test -p calternal-collab` ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.89s test result: ok. 45 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.27s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.58s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 33.56s test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.46s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.81s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.85s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.38s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.56s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.07s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.39s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` Collab focused event regression after fast-path change ``` test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 44 filtered out; finished in 4.33s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 11 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 15 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s ``` Server: `cargo clippy -p calternal-server --all-targets -- -D warnings`; `cargo test -p calternal-server` ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 28.46s test result: ok. 173 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 23.66s ``` Web: `bun run check`; `bunx vitest run src/lib/canvas/linkedText.test.ts src/lib/canvas/cards.test.ts src/lib/canvas/conversion.test.ts src/lib/canvas/conversionApi.test.ts --maxWorkers=2` ``` svelte-check found 0 errors and 0 warnings Test Files 4 passed (4) Tests 33 passed (33) ``` The production web build exited 0. It retains the existing exporter worker `import.meta` warnings. The focused production run used the job server and its real production web build: ``` CALTERNAL_SERVER_BIN=$CARGO_TARGET_DIR/debug/calternal-server CALTERNAL_E2E_ASSET_OVERRIDE=1 TMPDIR=$PWD/target/tmp node apps/web/e2e/canvas-text-977.mjs Canvas text: owner, Collaborate, Share, no item grant, revocation, concurrent edit, live refresh, duplicate identity pointer readonly guard, keyboard shared-item Open and twelve macOS captures passed. ``` Screenshots Twelve production captures emulate macOS: 390, 820 and 1440 px, light and dark, for read-only guidance and duplication. These are evidence for the orchestrator to review. No review artifact is committed. - [1440-dark-duplicate.png](https://git.kayg.org/attachments/595b3b2d-c90d-4c8c-a289-422660d2f383) - [1440-dark-readonly.png](https://git.kayg.org/attachments/012f6a8f-cf25-480e-8120-d895564bf6a5) - [1440-light-duplicate.png](https://git.kayg.org/attachments/5d010c78-2815-434c-b1a6-bfd2cac8d846) - [1440-light-readonly.png](https://git.kayg.org/attachments/52f2d16d-9357-46f0-a185-358ed2d9a488) - [390-dark-duplicate.png](https://git.kayg.org/attachments/bdf5bb78-08fa-4e65-8496-027b299af092) - [390-dark-readonly.png](https://git.kayg.org/attachments/319e5be6-7872-4c8f-afc2-0abb33caac4b) - [390-light-duplicate.png](https://git.kayg.org/attachments/5159cd1c-2989-48ae-a41d-2fb4ce309090) - [390-light-readonly.png](https://git.kayg.org/attachments/b6662116-ba8d-43bd-b12f-051484b77cb0) - [820-dark-duplicate.png](https://git.kayg.org/attachments/5e42b8e2-dcef-45fa-ac2d-69af6511917d) - [820-dark-readonly.png](https://git.kayg.org/attachments/a55862cc-8a52-4ed4-b6cc-155bc38d11ab) - [820-light-duplicate.png](https://git.kayg.org/attachments/97c3bf19-7413-45df-932c-6a2c7c7c2a43) - [820-light-readonly.png](https://git.kayg.org/attachments/463c5a21-812a-4664-8f01-c6247e459cd0) Performance No performance numbers are claimed. The later verification policy reserves performance runs for performance issues and the merge round. The extended profile is syntax checked. `docs/perf/baseline.json` has no Canvas card baseline to compare here. For the merge round - `cd apps/web && bun run test -- --maxWorkers=2`: run the combined web suite. - `cd apps/web && bun run test:e2e`: run the full shell suite with the combined production build. - `bash tests/adversarial/run.sh`: run the route, cross-User, authorization and robustness probes against the combined server. Check the new optional card fields and shared invalidations. - With the shared release server and production web assets on the perf VM: `flock /root/perf.lock node bench/canvas-cards-977.mjs --json artifacts/canvas-cards-977-profile.json`. The profile records load inside the lock. Record p50/p95, CPU and RSS; establish the Canvas baseline and check the burst. Never compile on that VM. - Review all twelve attached captures and check real macOS shared-item opening when the combined build is ready. Cleanup `cargo clean` output: ``` Removed 18161 files, 11.8GiB total ``` Deleted the web production build and Svelte output. Kept logs and screenshots in ignored `artifacts/`.
Author
Owner

Merge round 7c starts on job/merge-round-7c, base 4082669f7. The current owner job authorizes integration and full verification; it supersedes the original read-only #867 brief.

Canvas order: core → files → collab → Sketch → Pencil → cards. Migrations follow the combined 7b schema. No pushes or deploys. Final report will include verbatim gates, screenshots, defensive renderer review and staging readiness.

Merge round 7c starts on `job/merge-round-7c`, base `4082669f7`. The current owner job authorizes integration and full verification; it supersedes the original read-only #867 brief. Canvas order: core → files → collab → Sketch → Pencil → cards. Migrations follow the combined 7b schema. No pushes or deploys. Final report will include verbatim gates, screenshots, defensive renderer review and staging readiness.
Author
Owner

Finished merge-round-7c integration at 094d22e44507bf8bdd87dd8ffd460c254cb6329c. READY FOR STAGING: no.

Cards, conversion/deletion recovery and backlinks production flows pass. Linked text synchronization is not fully verified. Event reverse sync/Undo, Contact/web-link cards and frame reverse sync remain approved v1 follow-ups.

Production macOS evidence is attached to #867: 390/820/1440, light/dark. Screenshots remain outside git.

Focused Canvas viewer authority regression:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 248 filtered out; finished in 2.52s

Svelte check:

svelte-check found 0 errors and 4 warnings in 3 files

The full report, renderer boundary review, migration upgrade evidence, exact gate excerpts and decisions are in docs/audits/merge-round-7c.md and the final #867 comment. Staging blockers include the Notes process SIGSEGV after 278 passing assertions (#1069), stale performance exception pins, the retained thumbnail test conflict, Sketch save and unfinished verification. No pushes or deployments.

Finished merge-round-7c integration at `094d22e44507bf8bdd87dd8ffd460c254cb6329c`. READY FOR STAGING: no. Cards, conversion/deletion recovery and backlinks production flows pass. Linked text synchronization is not fully verified. Event reverse sync/Undo, Contact/web-link cards and frame reverse sync remain approved v1 follow-ups. Production macOS evidence is attached to [#867](https://git.kayg.org/kayg/calternal/issues/867): 390/820/1440, light/dark. Screenshots remain outside git. Focused Canvas viewer authority regression: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 248 filtered out; finished in 2.52s ``` Svelte check: ```text svelte-check found 0 errors and 4 warnings in 3 files ``` The full report, renderer boundary review, migration upgrade evidence, exact gate excerpts and decisions are in `docs/audits/merge-round-7c.md` and the final #867 comment. Staging blockers include the Notes process SIGSEGV after 278 passing assertions (#1069), stale performance exception pins, the retained thumbnail test conflict, Sketch save and unfinished verification. No pushes or deployments.
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#977
No description provided.