Sharing: open recipient stays read-only after editor grant #1405

Closed
opened 2026-10-07 21:52:04 +00:00 by kayg · 7 comments
Owner

Steps:

  1. Create an owner and a recipient User.
  2. Share a Note with the recipient as a viewer. Open that shared Note in the recipient's browser tab.
  3. In the owner's Share dialog, change the recipient's access to editor.
  4. Wait five seconds without reloading the recipient tab.

Expected: the open recipient view updates to Collaborate and allows editing.

Actual: the owner share list reports access: editor, but the recipient still sees “Read only” and has no editable surface ([contenteditable="true"] count is 0). The recipient's GET for the shared Note also reports access: "editor", so the stale access is in the open UI/session state.

Evidence: /home/kayg/Developer/calternal-wt/bash-core/artifacts/bash-core2/share-collaborate-access-stale.png; reproduced on the real local production build with target/tmp/share-collaborate-repro.mjs.

Severity: major. A live permission change does not take effect for an open recipient session. The issue is in the shared collaboration/editor boundary; the bash-core2 job did not edit that code.

Steps: 1. Create an owner and a recipient User. 2. Share a Note with the recipient as a viewer. Open that shared Note in the recipient's browser tab. 3. In the owner's Share dialog, change the recipient's access to editor. 4. Wait five seconds without reloading the recipient tab. Expected: the open recipient view updates to Collaborate and allows editing. Actual: the owner share list reports `access: editor`, but the recipient still sees “Read only” and has no editable surface (`[contenteditable="true"]` count is 0). The recipient's GET for the shared Note also reports `access: "editor"`, so the stale access is in the open UI/session state. Evidence: `/home/kayg/Developer/calternal-wt/bash-core/artifacts/bash-core2/share-collaborate-access-stale.png`; reproduced on the real local production build with `target/tmp/share-collaborate-repro.mjs`. Severity: major. A live permission change does not take effect for an open recipient session. The issue is in the shared collaboration/editor boundary; the bash-core2 job did not edit that code.
Author
Owner

Starting #1405 on job/grantlive-1405, based on origin/dev at d395b6e25da20d8c67b8b9fc1a593b813939c8a7. I’m tracing the shared collaboration access state and will cover both viewer→editor and editor→viewer in the recipient’s already-open browser view.

Starting #1405 on `job/grantlive-1405`, based on `origin/dev` at `d395b6e25da20d8c67b8b9fc1a593b813939c8a7`. I’m tracing the shared collaboration access state and will cover both viewer→editor and editor→viewer in the recipient’s already-open browser view.
Author
Owner

Finding for #1405: apps/web/e2e/share-access-1405.mjs reproduces the stale viewer state on the current production web build and shared server (d395b6e25). The owner changed access in the real Share dialog. After five seconds, the recipient GET returned access: "editor", but .note-card .cal-prose[contenteditable="true"] still matched 0 elements. The recipient stayed on the same /n/<id>?owner=<owner> route and still showed “Read only”. This confirms the access event and API read arrive; the shared Note reload reuses its original route snapshot.

Finding for #1405: `apps/web/e2e/share-access-1405.mjs` reproduces the stale viewer state on the current production web build and shared server (`d395b6e25`). The owner changed access in the real Share dialog. After five seconds, the recipient GET returned `access: "editor"`, but `.note-card .cal-prose[contenteditable="true"]` still matched 0 elements. The recipient stayed on the same `/n/<id>?owner=<owner>` route and still showed “Read only”. This confirms the access event and API read arrive; the shared Note reload reuses its original route snapshot.
Author
Owner

Further evidence for #1405: the real recipient browser received the Files SSE event for Shared/<owner>/Notes/<note>.md after the owner changed access, but it made no shared-Note read in response. The only shared-Note GET after the change was the explicit diagnostic read in the test. My earlier comment was incomplete: the route snapshot is stale, and the open Note refresh is also not reliably subscribed from its loaded grant state. I’m changing the shared Note owner to come from the stable route identity and using the fresh authorized response for either role transition.

Further evidence for #1405: the real recipient browser received the Files SSE event for `Shared/<owner>/Notes/<note>.md` after the owner changed access, but it made no shared-Note read in response. The only shared-Note GET after the change was the explicit diagnostic read in the test. My earlier comment was incomplete: the route snapshot is stale, and the open Note refresh is also not reliably subscribed from its loaded grant state. I’m changing the shared Note owner to come from the stable route identity and using the fresh authorized response for either role transition.
Author
Owner

Correction to the browser findings: the earlier #1405 browser run used the server's embedded dev assets. The test had not called routeCurrentBuild, so that run did not exercise this worktree's production web build. The regression now routes both browser contexts through the built worktree assets. I am rerunning the live viewer → editor → viewer flow before drawing a conclusion about the fix.

Correction to the browser findings: the earlier #1405 browser run used the server's embedded dev assets. The test had not called `routeCurrentBuild`, so that run did not exercise this worktree's production web build. The regression now routes both browser contexts through the built worktree assets. I am rerunning the live viewer → editor → viewer flow before drawing a conclusion about the fix.
Author
Owner

The real-browser regression now passes against the worktree's production web assets. The root cause was that an access refresh could reuse the route's original Note payload, so the recipient kept the old role after the owner changed it. The fix passes each newly authorized Note directly into the open view, keeps the owner-scoped subscription after revocation, and rejects stale or unrelated refreshes. The test upgrades and downgrades the same open recipient view, then verifies a real edit is visible to the owner and persisted.

The real-browser regression now passes against the worktree's production web assets. The root cause was that an access refresh could reuse the route's original Note payload, so the recipient kept the old role after the owner changed it. The fix passes each newly authorized Note directly into the open view, keeps the owner-scoped subscription after revocation, and rejects stale or unrelated refreshes. The test upgrades and downgrades the same open recipient view, then verifies a real edit is visible to the owner and persisted.
Author
Owner

Completed: #1405

The recipient’s open Note now follows viewer → editor → viewer role changes without a reload. The root cause was reuse of the initial route Note payload after a grant change. The refresh now passes the newly authorized Note into the open view, keeps an owner-scoped subscription after revocation so a restored grant can recover the view, filters unrelated owner paths, and fences stale responses by route and generation. A revoke clears the body on 404.

Commits

  • 6f85568d89b8f526aa64445a97d254e65cb0f08d — fix the open shared Note access refresh and add the real-browser regression.
  • d985dd8394946192aa904c87eb30cc23560e03e1 — refresh the exact NoteView performance pins without changing their rules or limits.
  • 4aeda648215553d44672f2508e8e19729829674c — add the shared Note access hot-path profile and local measurements.
  • 442deea35be2dc366bcd2bcb4b84baa39cb39108 — clarify the profile’s invalidation scope.

Head: 442deea35be2dc366bcd2bcb4b84baa39cb39108

Files

  • apps/web/src/lib/notes/NoteView.svelte
  • apps/web/e2e/share-access-1405.mjs
  • bench/groups-grants.mjs
  • docs/perf/baseline.json
  • contracts/perf/adoption-1058.json
  • contracts/perf/exceptions.json

Browser proof

The real-browser regression leaves the recipient on one Note route, changes the role through the owner’s Share dialog, checks both live surfaces and the grant API, makes a real edit as editor, and confirms the owner sees it and the source persists. It then downgrades the same open recipient back to viewer. The test passes:

PASS #1405: an open recipient follows viewer→editor→viewer without a reload; both live surfaces and real edits agree

The archive has all 42 actual-size screenshots with macOS platform emulation at 390, 820, and 1440 px, in light and dark themes: download all screenshots.

Closest existing screen and recipient states:

Owner Note reference, 1440 px, light
Recipient viewer, 1440 px, light
Recipient editor, 1440 px, light
Recipient after downgrade, 1440 px, light

Share dialog interaction captures:

Share dialog after pointer opening
Share dialog after changing access by click
Share dialog after keyboard shortcut opening

Gate output

cargo fmt --check completed with exit 0 and no output. No Rust crate changed, so no crate-specific cargo clippy or cargo test ran.

bun run check completed with exit 0. Relevant output, verbatim:

PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages
perf-lint: PASS; 0 violations; 22544 scoped exceptions
svelte-check found 0 errors and 4 warnings in 2 files

bun run test output, verbatim:

Test Files  297 passed (297)
      Tests  2116 passed (2116)
   Start at  08:55:44
   Duration  825.07s (environment 27%, transform 25%, import 25%, tests 17%, setup 6%)

The full suite reached Vitest only after its preceding && checks passed. The capture wrapper then reported zsh:1: read-only variable: status while trying to store the completed command’s exit code; Vitest’s completed summary above reports every file and test passed.

Performance profile

The profile measures one owner-filtered Note identity lookup per Files invalidation in the shared owner namespace. It uses the production SQL and migration set. Local shared-host results recorded in docs/perf/baseline.json:

  • 10 Notes, 2,000 requests: p50 0.012801 ms, p95 0.031169 ms, CPU 47.177 ms, RSS 36,757,504 bytes.
  • 10,000 Notes and 100 Groups, 1,000-request burst: p50 0.342201 ms, p95 0.480771 ms, CPU 351.12 ms, RSS 134,336,512 bytes.
  • Load average was 43.74 before and 45.74 after, so these local measurements are a noisy reference.

The existing profile had extracted the five-bind Note query but supplied three values (SQLite query expected 5 values, received 3). The profile now measures the owner-filtered Note lookup with the correct identity, recipient, and owner binds.

UX gaps closed

  • The same open Note updates in both directions without navigation or reload.
  • The test performs an actual edit as editor and checks the owner’s visible copy and persisted source.
  • The Share dialog is captured after pointer opening, access selection by click, and keyboard shortcut opening. Screens show the real loaded UI without a hover overlay or focus ring.
  • The full width/theme set and the closest existing Note surface are attached above.

UX gaps left: none found in this flow.

Decisions

DESIGN §54 requires the recipient to update when access changes in place but does not define a live subscription’s authority. The subscription retains the route’s stable owner only as a lookup scope; every event re-reads access from the server before changing the open Note. Responses are generation-fenced so an older read cannot restore stale access.

# Completed: #1405 The recipient’s open Note now follows viewer → editor → viewer role changes without a reload. The root cause was reuse of the initial route Note payload after a grant change. The refresh now passes the newly authorized Note into the open view, keeps an owner-scoped subscription after revocation so a restored grant can recover the view, filters unrelated owner paths, and fences stale responses by route and generation. A revoke clears the body on 404. ## Commits - `6f85568d89b8f526aa64445a97d254e65cb0f08d` — fix the open shared Note access refresh and add the real-browser regression. - `d985dd8394946192aa904c87eb30cc23560e03e1` — refresh the exact NoteView performance pins without changing their rules or limits. - `4aeda648215553d44672f2508e8e19729829674c` — add the shared Note access hot-path profile and local measurements. - `442deea35be2dc366bcd2bcb4b84baa39cb39108` — clarify the profile’s invalidation scope. Head: `442deea35be2dc366bcd2bcb4b84baa39cb39108` ## Files - `apps/web/src/lib/notes/NoteView.svelte` - `apps/web/e2e/share-access-1405.mjs` - `bench/groups-grants.mjs` - `docs/perf/baseline.json` - `contracts/perf/adoption-1058.json` - `contracts/perf/exceptions.json` ## Browser proof The real-browser regression leaves the recipient on one Note route, changes the role through the owner’s Share dialog, checks both live surfaces and the grant API, makes a real edit as editor, and confirms the owner sees it and the source persists. It then downgrades the same open recipient back to viewer. The test passes: `PASS #1405: an open recipient follows viewer→editor→viewer without a reload; both live surfaces and real edits agree` The archive has all 42 actual-size screenshots with macOS platform emulation at 390, 820, and 1440 px, in light and dark themes: [download all screenshots](https://git.kayg.org/attachments/ccd6c491-3a89-4a50-a98b-e9d5ddba367e). Closest existing screen and recipient states: ![Owner Note reference, 1440 px, light](https://git.kayg.org/attachments/64181286-8286-419f-b353-5c697435a227) ![Recipient viewer, 1440 px, light](https://git.kayg.org/attachments/3de5d784-e1cd-411b-acb1-7fa89c5715c9) ![Recipient editor, 1440 px, light](https://git.kayg.org/attachments/30b874a8-0ecd-4cb4-a331-385fa7783d92) ![Recipient after downgrade, 1440 px, light](https://git.kayg.org/attachments/f1935d2c-654a-4507-b004-db6a86c0653c) Share dialog interaction captures: ![Share dialog after pointer opening](https://git.kayg.org/attachments/b40d17d7-7766-436b-9f72-6fd28984d4a6) ![Share dialog after changing access by click](https://git.kayg.org/attachments/84ff6a7a-77cc-4f8c-8378-8182ef033bf5) ![Share dialog after keyboard shortcut opening](https://git.kayg.org/attachments/7188523c-7869-40b7-a4d6-35b9ca1ffe09) ## Gate output `cargo fmt --check` completed with exit 0 and no output. No Rust crate changed, so no crate-specific `cargo clippy` or `cargo test` ran. `bun run check` completed with exit 0. Relevant output, verbatim: ```text PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages perf-lint: PASS; 0 violations; 22544 scoped exceptions svelte-check found 0 errors and 4 warnings in 2 files ``` `bun run test` output, verbatim: ```text Test Files 297 passed (297) Tests 2116 passed (2116) Start at 08:55:44 Duration 825.07s (environment 27%, transform 25%, import 25%, tests 17%, setup 6%) ``` The full suite reached Vitest only after its preceding `&&` checks passed. The capture wrapper then reported `zsh:1: read-only variable: status` while trying to store the completed command’s exit code; Vitest’s completed summary above reports every file and test passed. ## Performance profile The profile measures one owner-filtered Note identity lookup per Files invalidation in the shared owner namespace. It uses the production SQL and migration set. Local shared-host results recorded in `docs/perf/baseline.json`: - 10 Notes, 2,000 requests: p50 `0.012801 ms`, p95 `0.031169 ms`, CPU `47.177 ms`, RSS `36,757,504 bytes`. - 10,000 Notes and 100 Groups, 1,000-request burst: p50 `0.342201 ms`, p95 `0.480771 ms`, CPU `351.12 ms`, RSS `134,336,512 bytes`. - Load average was `43.74` before and `45.74` after, so these local measurements are a noisy reference. The existing profile had extracted the five-bind Note query but supplied three values (`SQLite query expected 5 values, received 3`). The profile now measures the owner-filtered Note lookup with the correct identity, recipient, and owner binds. ## UX gaps closed - The same open Note updates in both directions without navigation or reload. - The test performs an actual edit as editor and checks the owner’s visible copy and persisted source. - The Share dialog is captured after pointer opening, access selection by click, and keyboard shortcut opening. Screens show the real loaded UI without a hover overlay or focus ring. - The full width/theme set and the closest existing Note surface are attached above. UX gaps left: none found in this flow. ## Decisions DESIGN §54 requires the recipient to update when access changes in place but does not define a live subscription’s authority. The subscription retains the route’s stable owner only as a lookup scope; every event re-reads access from the server before changing the open Note. Responses are generation-fenced so an older read cannot restore stale access.
Author
Owner

Deployed to calternal.cloud in merge round 35a (build cc1af3c12, live 2026-10-09 00:12 IST). Closing.

Deployed to calternal.cloud in merge round 35a (build cc1af3c12, live 2026-10-09 00:12 IST). Closing.
kayg closed this issue 2026-10-08 18:41:29 +00:00
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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-v0#1405
No description provided.