Fewer toasts: only actionable ones, and every toast also goes to Notifications #1142

Open
opened 2026-10-05 12:38:31 +00:00 by kayg · 7 comments
Owner

Owner decision (2026-10-05)

"Every toast should have a notification too, but in general we should show fewer toasts. Right now we show toasts on literally everything."

Rule:

  • Show a toast only when the User can still act on it (Undo, Retry, Open) or for a background result the User is waiting for (import done, upload failed).
  • No toast for confirmations already visible on screen ("Saved", "Tag added", "Copied link" when the button itself confirms).
  • Every toast that is shown is also written to Notifications (same text, same action while it is still valid), so nothing is lost when it fades.

Do: audit every toast( call in apps/web and packages; classify keep / drop / replace-with-inline-confirmation; route the shared toast primitive through Notifications; add a design-rule test that fails on a new toast without an action unless it is on an allow-list of background results. Screenshots of the changed flows.

## Owner decision (2026-10-05) "Every toast should have a notification too, but in general we should show fewer toasts. Right now we show toasts on literally everything." Rule: - Show a toast only when the User can still act on it (Undo, Retry, Open) or for a background result the User is waiting for (import done, upload failed). - No toast for confirmations already visible on screen ("Saved", "Tag added", "Copied link" when the button itself confirms). - Every toast that is shown is also written to Notifications (same text, same action while it is still valid), so nothing is lost when it fades. Do: audit every `toast(` call in apps/web and packages; classify keep / drop / replace-with-inline-confirmation; route the shared toast primitive through Notifications; add a design-rule test that fails on a new toast without an action unless it is on an allow-list of background results. Screenshots of the changed flows.
Author
Owner

Starting issue #1142 on branch job/toast-1142, based on 04afb0043c559c59084e4ec12609bb9fe679cbf6. I will audit web and package toast calls, update the shared toast/Notifications behavior and rule test, and capture the changed flows.

Starting issue #1142 on branch `job/toast-1142`, based on `04afb0043c559c59084e4ec12609bb9fe679cbf6`. I will audit web and package toast calls, update the shared toast/Notifications behavior and rule test, and capture the changed flows.
Author
Owner

Audit finding: a source scan found 342 production toast(...) invocations in apps/web; 67 include an action. The Notifications API exposes inbox reads and server-produced event writes only, so the web toast adapter currently has no durable inbox path. I will add a User-scoped, rate-limited route for web toast notices; toast actions will be available in the inbox only while the same live action handler exists.

Audit finding: a source scan found 342 production `toast(...)` invocations in apps/web; 67 include an action. The Notifications API exposes inbox reads and server-produced event writes only, so the web toast adapter currently has no durable inbox path. I will add a User-scoped, rate-limited route for web toast notices; toast actions will be available in the inbox only while the same live action handler exists.
Author
Owner

Verification finding: the focused toast and inbox Vitest files pass 16/16. bun run check stops at scripts/perf-lint --check before Svelte checking because the committed perf exception ledger has 21,977 entries while its committed ratchet says 21,964. The same mismatch is present at both this branch HEAD and origin/dev (for example, contract.blaze has 290 entries against a ceiling of 289), so I am preserving the existing ratchet and will run Svelte checking directly.

Verification finding: the focused toast and inbox Vitest files pass 16/16. `bun run check` stops at `scripts/perf-lint --check` before Svelte checking because the committed perf exception ledger has 21,977 entries while its committed ratchet says 21,964. The same mismatch is present at both this branch HEAD and `origin/dev` (for example, `contract.blaze` has 290 entries against a ceiling of 289), so I am preserving the existing ratchet and will run Svelte checking directly.
Author
Owner

Audit finding: the Files copy/cut buffer and Finder paste limitation had been silenced as redundant toasts, but neither had replacement feedback. I added a persistent inline buffer status, plus an accessible Finder paste notice with a Choose files action; passkey-link copy now confirms inline and exposes a Retry action when clipboard write fails.

Audit finding: the Files copy/cut buffer and Finder paste limitation had been silenced as redundant toasts, but neither had replacement feedback. I added a persistent inline buffer status, plus an accessible Finder paste notice with a Choose files action; passkey-link copy now confirms inline and exposes a Retry action when clipboard write fails.
Author
Owner

Post-merge rule-test finding: the updated Money Undo flow had two actionless notices at the account register's offerUndo path: a changed transaction made Undo unavailable, and an Undo write could fail. These are outcomes of the Undo action the User just selected, so both remain as background-result notices with the #1142 classification. The concurrent-change and write-failure behavior is unchanged.

Post-merge rule-test finding: the updated Money Undo flow had two actionless notices at the account register's `offerUndo` path: a changed transaction made Undo unavailable, and an Undo write could fail. These are outcomes of the Undo action the User just selected, so both remain as background-result notices with the #1142 classification. The concurrent-change and write-failure behavior is unchanged.
Author
Owner

Adversarial contract finding: the new toast operation was absent from XUser identity classification because event_id matched the route inventory's identity-field rule. I classified it as a client-generated idempotency key scoped by authenticated User, and the focused route probe reuses one event ID for two Users to verify distinct inbox rows. The offline classification gate now reports 409 operations and 1,149 generated entry points classified; the admin identity gate reports 48 reviewed operations with route and Rust guards in agreement.

Adversarial contract finding: the new toast operation was absent from XUser identity classification because `event_id` matched the route inventory's identity-field rule. I classified it as a client-generated idempotency key scoped by authenticated User, and the focused route probe reuses one event ID for two Users to verify distinct inbox rows. The offline classification gate now reports 409 operations and 1,149 generated entry points classified; the admin identity gate reports 48 reviewed operations with route and Rust guards in agreement.
Author
Owner

#1142 complete

Branch: job/toast-1142
Base: 04afb0043c559c59084e4ec12609bb9fe679cbf6 (dev)
Head: d9094882db89594c487d208319a4e71f9c89e040

Built

  • Audited production toast( calls across apps/web and packages. The shared adapter now suppresses explicit policy: 'drop' results and unclassified calls. Actionable and allow-listed background-result toasts are persisted to the authenticated User's Notifications inbox with matching copy and action; callback actions remain available while live, then stable local links remain usable.
  • Added the web_toast inbox kind and idempotent POST /api/v1/notifications/inbox/toasts route. The route validates content and local links, isolates rows by User, and enforces per-User request limits. Toast notices do not send push notifications.
  • Added the #1142 rule to docs/DESIGN.md §28 and a design-rule test that scans production call sites. Added OpenAPI/action-policy output, a Notifications write benchmark profile, and a focused adversarial probe.
  • Replaced redundant copy confirmations with inline feedback in Files and passkey flows. Preserved actionable Undo/Retry/Open outcomes and background completion/failure results.

Key files: apps/web/src/lib/stores/toasts.svelte.ts, apps/web/src/lib/stores/toast-policy.test.ts, apps/web/src/lib/notifications/InboxPanel.svelte, apps/web/src/lib/notifications/inbox.svelte.ts, apps/web/src/lib/files/UploadToast.svelte, apps/web/e2e/toast-policy-1142.mjs, crates/plugins/notifications/{migrations/0007_web_toasts.sql,src/routes.rs,src/store.rs}, contracts/{action-policy.json,openapi.json}, packages/api-client/src/generated.ts, docs/DESIGN.md, bench/toast-1142.mjs, and tests/adversarial/toast_notifications.mjs. Toast call-site classifications also touch Calendar, Canvas, Composer, Files, History, Location, Mail, Notes, Notifications, Photos, Search, Tasks, Money and Settings modules.

UX gaps closed

  • Files copy/cut and unsupported Finder paste now give visible inline status instead of silent actions.
  • Passkey link copy confirms inline; clipboard failure offers Retry.
  • Undo, Retry and Open remain available on the toast and in Notifications while their actions are valid. Upload progress/result copy stays on one event ID and one inbox row.
  • The screenshot walkthrough covers toast, inbox and real upload result at 390/820/1440 px in light/dark with macOS platform emulation. It captured 24 production screenshots in artifacts/toast-1142/.

UX gaps left

  • scripts/fj issue comment --help exposes body text only and has no attachment option. The 24 screenshots remain in artifacts/toast-1142/; this CLI could not upload them to this comment.
  • The full server test run had one startup timing failure under shared-host load. Two migration inventory assertions also failed in that run because migration 0007 was newly added; I updated those expectations, and both focused migration tests pass. I did not rerun the full server suite.
  • bun run check still stops at the committed perf-lint ratchet mismatch also present on origin/dev; direct svelte-check reports zero errors.
  • bench/toast-1142.mjs is present but unmeasured. This issue is not a performance issue, and the job verification policy defers perf measurements.

Decisions for owner confirmation

  • Persist these as web_toast Notifications rows with a User-scoped UUID event ID, stable link fallback, and no push delivery.
  • Allow actionless result toasts only for import-complete, upload-failed, download-ready, sync-result, and background-task-failed.
  • Use a budget of 120 distinct event IDs per User per minute and 600 total requests per User per minute; title max 1000 bytes, action max 64 bytes, and local link max 2048 bytes.

Gates

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

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

    Checking calternal-plugin-notifications v0.0.1 (/home/kayg/Developer/calternal-wt/toast-1142/crates/plugins/notifications)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 43.48s

cargo test -p calternal-plugin-notifications: 37 passed, 0 failed; doc tests 0.

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

   Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/toast-1142/crates/calternal-server)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 28.13s

cargo test -p calternal-server (full run before updating the migration fixture expectations):

test result: FAILED. 253 passed; 3 failed; 10 ignored; 0 measured; 0 filtered out; finished in 89.38s
error: test failed, to rerun pass `-p calternal-server --bin calternal-server`

The remaining runtime failure was wire::tests::startup_serves_http_while_upgrade_backfills_wait, which timed out with Elapsed(()) while wire::tests::live_apps_run_in_separate_processes was running. After adjusting migration inventory expectations, both production_round_9_schema_upgrades_once_with_original_receipts and production_7c_upgrades_content_types_once passed individually.

Focused Vitest:

 Test Files  3 passed (3)
      Tests  17 passed (17)

bunx svelte-check --tsconfig ./tsconfig.json:

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

Warnings are two existing empty CSS rules in calendar components and two unused Notes selectors.

bun run build exited 0 and reported:

Compressed 885 static variants; saved 20927606 bytes.

PYTHONPATH=. python3 bench/test_record.py:

Ran 14 tests in 0.115s

OK

Production screenshot walkthrough:

SCREENSHOT 390px light macOS: Moved an item to the Trash · 390 · paper
SCREENSHOT 390px dark macOS: Moved an item to the Trash · 390 · tokyo-night
SCREENSHOT 820px light macOS: Moved an item to the Trash · 820 · paper
SCREENSHOT 820px dark macOS: Moved an item to the Trash · 820 · tokyo-night
SCREENSHOT 1440px light macOS: Moved an item to the Trash · 1440 · paper
SCREENSHOT 1440px dark macOS: Moved an item to the Trash · 1440 · tokyo-night
PASS captured twenty-four production screenshots in /home/kayg/Developer/calternal-wt/toast-1142/artifacts/toast-1142

Focused adversarial output:

Cross-User classification gate: 409 operations classified
Generated entry point classification: 1149 tools classified
Admin coverage: 48 reviewed operations; contract and Rust guards agree
Focused toast notice fixture ready
PASS #1142 toast route auth, validation, idempotency, User isolation and concurrent writes

bun run check output:

perf-lint: INVALID: exception ratchet: contract.blaze: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; contract.blocked-network: 356 exceptions exceed the ratchet ceiling 355; fix the new violation instead; contract.dom-bound: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; contract.model-bound: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; contract.row-identity: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; contract.snapshot: 356 exceptions exceed the ratchet ceiling 355; fix the new violation instead; coverage.profile: 356 exceptions exceed the ratchet ceiling 355; fix the new violation instead; coverage.readiness: 356 exceptions exceed the ratchet ceiling 355; fix the new violation instead; io.unresolved-call: 16191 exceptions exceed the ratchet ceiling 16190; fix the new violation instead; render.blaze-adapter: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; render.bound: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; render.model_byte_cap: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; render.model_row_cap: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; total: ledger has 21977 exceptions; ratchet total is 21964

The same committed ledger/ratchet mismatch is present on origin/dev; no ratchet change was made.

For the merge round

  • cd apps/web && bun run test — full web suite on the combined branch.
  • cd apps/web && bun run test:e2e — shared production shell e2e.
  • tests/adversarial/run.sh — full XUser, authz and robustness matrices, including the toast route across the combined branch.
#1142 complete Branch: `job/toast-1142` Base: `04afb0043c559c59084e4ec12609bb9fe679cbf6` (`dev`) Head: `d9094882db89594c487d208319a4e71f9c89e040` ## Built - Audited production `toast(` calls across `apps/web` and packages. The shared adapter now suppresses explicit `policy: 'drop'` results and unclassified calls. Actionable and allow-listed background-result toasts are persisted to the authenticated User's Notifications inbox with matching copy and action; callback actions remain available while live, then stable local links remain usable. - Added the `web_toast` inbox kind and idempotent `POST /api/v1/notifications/inbox/toasts` route. The route validates content and local links, isolates rows by User, and enforces per-User request limits. Toast notices do not send push notifications. - Added the #1142 rule to `docs/DESIGN.md` §28 and a design-rule test that scans production call sites. Added OpenAPI/action-policy output, a Notifications write benchmark profile, and a focused adversarial probe. - Replaced redundant copy confirmations with inline feedback in Files and passkey flows. Preserved actionable Undo/Retry/Open outcomes and background completion/failure results. Key files: `apps/web/src/lib/stores/toasts.svelte.ts`, `apps/web/src/lib/stores/toast-policy.test.ts`, `apps/web/src/lib/notifications/InboxPanel.svelte`, `apps/web/src/lib/notifications/inbox.svelte.ts`, `apps/web/src/lib/files/UploadToast.svelte`, `apps/web/e2e/toast-policy-1142.mjs`, `crates/plugins/notifications/{migrations/0007_web_toasts.sql,src/routes.rs,src/store.rs}`, `contracts/{action-policy.json,openapi.json}`, `packages/api-client/src/generated.ts`, `docs/DESIGN.md`, `bench/toast-1142.mjs`, and `tests/adversarial/toast_notifications.mjs`. Toast call-site classifications also touch Calendar, Canvas, Composer, Files, History, Location, Mail, Notes, Notifications, Photos, Search, Tasks, Money and Settings modules. ## UX gaps closed - Files copy/cut and unsupported Finder paste now give visible inline status instead of silent actions. - Passkey link copy confirms inline; clipboard failure offers Retry. - Undo, Retry and Open remain available on the toast and in Notifications while their actions are valid. Upload progress/result copy stays on one event ID and one inbox row. - The screenshot walkthrough covers toast, inbox and real upload result at 390/820/1440 px in light/dark with macOS platform emulation. It captured 24 production screenshots in `artifacts/toast-1142/`. ## UX gaps left - `scripts/fj issue comment --help` exposes body text only and has no attachment option. The 24 screenshots remain in `artifacts/toast-1142/`; this CLI could not upload them to this comment. - The full server test run had one startup timing failure under shared-host load. Two migration inventory assertions also failed in that run because migration 0007 was newly added; I updated those expectations, and both focused migration tests pass. I did not rerun the full server suite. - `bun run check` still stops at the committed perf-lint ratchet mismatch also present on `origin/dev`; direct `svelte-check` reports zero errors. - `bench/toast-1142.mjs` is present but unmeasured. This issue is not a performance issue, and the job verification policy defers perf measurements. ## Decisions for owner confirmation - Persist these as `web_toast` Notifications rows with a User-scoped UUID event ID, stable link fallback, and no push delivery. - Allow actionless result toasts only for `import-complete`, `upload-failed`, `download-ready`, `sync-result`, and `background-task-failed`. - Use a budget of 120 distinct event IDs per User per minute and 600 total requests per User per minute; title max 1000 bytes, action max 64 bytes, and local link max 2048 bytes. ## Gates `cargo fmt --check`: exit 0, no output. `cargo clippy -p calternal-plugin-notifications --all-targets -- -D warnings`: ```text Checking calternal-plugin-notifications v0.0.1 (/home/kayg/Developer/calternal-wt/toast-1142/crates/plugins/notifications) Finished `dev` profile [unoptimized + debuginfo] target(s) in 43.48s ``` `cargo test -p calternal-plugin-notifications`: 37 passed, 0 failed; doc tests 0. `cargo clippy -p calternal-server --all-targets -- -D warnings`: ```text Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/toast-1142/crates/calternal-server) Finished `dev` profile [unoptimized + debuginfo] target(s) in 28.13s ``` `cargo test -p calternal-server` (full run before updating the migration fixture expectations): ```text test result: FAILED. 253 passed; 3 failed; 10 ignored; 0 measured; 0 filtered out; finished in 89.38s error: test failed, to rerun pass `-p calternal-server --bin calternal-server` ``` The remaining runtime failure was `wire::tests::startup_serves_http_while_upgrade_backfills_wait`, which timed out with `Elapsed(())` while `wire::tests::live_apps_run_in_separate_processes` was running. After adjusting migration inventory expectations, both `production_round_9_schema_upgrades_once_with_original_receipts` and `production_7c_upgrades_content_types_once` passed individually. Focused Vitest: ```text Test Files 3 passed (3) Tests 17 passed (17) ``` `bunx svelte-check --tsconfig ./tsconfig.json`: ```text svelte-check found 0 errors and 4 warnings in 3 files ``` Warnings are two existing empty CSS rules in calendar components and two unused Notes selectors. `bun run build` exited 0 and reported: ```text Compressed 885 static variants; saved 20927606 bytes. ``` `PYTHONPATH=. python3 bench/test_record.py`: ```text Ran 14 tests in 0.115s OK ``` Production screenshot walkthrough: ```text SCREENSHOT 390px light macOS: Moved an item to the Trash · 390 · paper SCREENSHOT 390px dark macOS: Moved an item to the Trash · 390 · tokyo-night SCREENSHOT 820px light macOS: Moved an item to the Trash · 820 · paper SCREENSHOT 820px dark macOS: Moved an item to the Trash · 820 · tokyo-night SCREENSHOT 1440px light macOS: Moved an item to the Trash · 1440 · paper SCREENSHOT 1440px dark macOS: Moved an item to the Trash · 1440 · tokyo-night PASS captured twenty-four production screenshots in /home/kayg/Developer/calternal-wt/toast-1142/artifacts/toast-1142 ``` Focused adversarial output: ```text Cross-User classification gate: 409 operations classified Generated entry point classification: 1149 tools classified Admin coverage: 48 reviewed operations; contract and Rust guards agree Focused toast notice fixture ready PASS #1142 toast route auth, validation, idempotency, User isolation and concurrent writes ``` `bun run check` output: ```text perf-lint: INVALID: exception ratchet: contract.blaze: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; contract.blocked-network: 356 exceptions exceed the ratchet ceiling 355; fix the new violation instead; contract.dom-bound: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; contract.model-bound: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; contract.row-identity: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; contract.snapshot: 356 exceptions exceed the ratchet ceiling 355; fix the new violation instead; coverage.profile: 356 exceptions exceed the ratchet ceiling 355; fix the new violation instead; coverage.readiness: 356 exceptions exceed the ratchet ceiling 355; fix the new violation instead; io.unresolved-call: 16191 exceptions exceed the ratchet ceiling 16190; fix the new violation instead; render.blaze-adapter: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; render.bound: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; render.model_byte_cap: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; render.model_row_cap: 290 exceptions exceed the ratchet ceiling 289; fix the new violation instead; total: ledger has 21977 exceptions; ratchet total is 21964 ``` The same committed ledger/ratchet mismatch is present on `origin/dev`; no ratchet change was made. ## For the merge round - `cd apps/web && bun run test` — full web suite on the combined branch. - `cd apps/web && bun run test:e2e` — shared production shell e2e. - `tests/adversarial/run.sh` — full XUser, authz and robustness matrices, including the toast route across the combined branch.
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#1142
No description provided.