{ "sourceCaptureSHA256": "1988a0af219e86a020ec09bd320f8ff02c7d3685f010927ccf725b79f105c058", "projection": "Exact public file-mutation inputs and identity/timestamp/success metadata; result bodies and unrelated tools omitted. Current before and pane are direct retained bytes.", "cwd": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-autoplan-chain-RnwL2i", "config": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/with-skills/.claude", "stateRoot": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/skill-home-zgCNxG/.gstack", "commandStartedAt": 1789032380903, "now": 1789033507687, "viewport": " +o CC figure given). Deferred: needs a ranking service and push infrastructure that do not exist. This p\n +lan lays the substrate it would build on: the per-panel result envelope, the per-panel state machine (l\n +oading \u2192 ok / empty / error \u2192 retry), and the instrumentation; all three are in Accepted Scope below. \n 24 \n 25 ## Scope Decisions\n 26 \n\n\u25cf Update(~/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md)\n\n\u25cf Update(~/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md)\n\n\u25cf Update(~/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md)\n\n\u25cf Update(~/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md)\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Edit file\n \u20262101964-HvDZyN/skill-home-zgCNxG/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md\n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n 26 \n 27 | # | Proposal | Effort | Decision | Reasoning | Revisit when |\n 28 |---|----------|--------|----------|-----------|--------------|\n 29 -| 1 | Pull real login-to-first-task baseline (median/p90) and find-vs-do split from existing analytics before locki\n -ng 45s | S | ACCEPTED | Data already exists; the 75s walkthrough number is a stand-in | \u2014 | \n 29 +| 1 | Pull real login-to-first-task baseline (median/p90) and find-vs-do split from existing analytics before locki\n +ng 45s | S | ACCEPTED | Data already exists; the 75s walkthrough number is a stand-in | Dashboard owner re-locks th\n +e target in this document after the pull | \n 30 | 2 | Numeric rollback triggers defined before rollout | S | ACCEPTED | Rollout criteria were \"to be specified\"; de\n pends on #1 | \u2014 |\n 31 | 3 | Relative timestamps with absolute on hover/focus in ActivityFeed | S | ACCEPTED | 1 file, under an hour, in b\n last radius | \u2014 |\n 32 | 4 | Unread count badge and document.title mirror | S | ACCEPTED | 1 file, under an hour | \u2014 |\n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n Do you want to make this edit to 2026-09-10-user-dashboard.md?\n \u276f 1. Yes\n 2. Yes, and switch to accept edits (auto-approve file edits and common file commands) for this session (shift+tab)\n 3. No\n\n Esc to cancel \u00b7 Tab to amend\n", "before": "---\nstatus: ACTIVE\n---\n# CEO Plan: User Dashboard Page\nGenerated by /plan-ceo-review (via /autoplan) on 2026-09-10\nBranch: main | Mode: SELECTIVE EXPANSION (hold the plan's scope as baseline; cherry-pick expansions individually)\nRepo: gstack-autoplan-chain-RnwL2i (no remote)\nSource plan: `.claude/plans/ui-heavy-feature.md` (reviewed copy with full review record: `.claude/plans/starry-riding-otter.md`)\n\n**Primary metric:** median login-to-first-completed-task \u2264 45s (provisional until the baseline in #1 is pulled). Guardrails: completed-task rate and permission-error rate must not regress.\n\n**Glossary.** *Blast radius*: the files this plan creates or modifies plus their direct importers. *Find vs. do*: time from login to starting an action, versus time from starting to completing it. *CC*: Claude Code implementation time, as opposed to human-team time. *Effort scale* (human team): S under 1 day, M 1 to 5 days, L 1 to 3 weeks, XL over 3 weeks. *Previous landing page*: the post-login destination in use before this plan (the existing default route members see today; name it in the flag config when implementing).\n\n**Acceptance principle.** In SELECTIVE EXPANSION, an expansion inside the blast radius that costs under an hour is accepted on cost alone, whether or not it moves the metric (#3, #4, #6). Items outside the blast radius, or that need an audit or new infrastructure, are deferred even when cheap (#7, #8).\n\n**Assumptions.** The feature-flag framework, request/error metrics, and analytics events named in the source plan exist (this repository contains no source, so they are unverified). Item #1 (baseline pull) precedes item #2 (numeric rollback triggers), because the triggers are expressed against the baseline. **Fallback if the analytics events do not exist:** instrument login, action start, and action completion first, collect at least 7 days of data before any cohort rollout, and keep the 75s walkthrough figure as the stand-in with the target widened to \"at least 30% faster than measured baseline\" until the pull succeeds.\n\n**Carried from the source plan (not additions):** the per-panel state machine (loading \u2192 ok / empty / error \u2192 retry) and the instrumentation set (metrics, alerts, structured logs) are already required by the source plan's accepted CEO obligations; this document ratifies them without a proposal row.\n\n## Vision\n\n### 10x Check\nA post-login home that tells the member what to do next instead of showing three things to scan. A ranked \"next up\" card sits above the panels, computed server-side from eligible actions and unread alerts, and updates live over a push channel. The member arrives, sees one thing, and does it. Effort: XL, infrastructure-bound rather than implementation-bound (ranking service and push channel must exist first; no CC figure given). Deferred: needs a ranking service and push infrastructure that do not exist. This plan lays the substrate it would build on: the per-panel result envelope, the per-panel state machine (loading \u2192 ok / empty / error \u2192 retry), and the instrumentation; all three are in Accepted Scope below.\n\n## Scope Decisions\n\n| # | Proposal | Effort | Decision | Reasoning | Revisit when |\n|---|----------|--------|----------|-----------|--------------|\n| 1 | Pull real login-to-first-task baseline (median/p90) and find-vs-do split from existing analytics before locking 45s | S | ACCEPTED | Data already exists; the 75s walkthrough number is a stand-in | \u2014 |\n| 2 | Numeric rollback triggers defined before rollout | S | ACCEPTED | Rollout criteria were \"to be specified\"; depends on #1 | \u2014 |\n| 3 | Relative timestamps with absolute on hover/focus in ActivityFeed | S | ACCEPTED | 1 file, under an hour, in blast radius | \u2014 |\n| 4 | Unread count badge and document.title mirror | S | ACCEPTED | 1 file, under an hour | \u2014 |\n| 5 | \"Back to previous landing page\" link during rollout | S | ACCEPTED | Per-member escape hatch and bounce-back signal | Remove at 100% rollout |\n| 6 | Empty-state copy pointing at the primary action | S | ACCEPTED | Copy only | \u2014 |\n| 7 | Keyboard shortcuts for quick actions | S | DEFERRED | Shortcut conflict audit needed; not on the metric path | Dashboard owner runs the conflict audit after 100% rollout |\n| 8 | Prefetch /api/dashboard during login redirect | S | DEFERRED | Touches login flow, outside blast radius | If client TTFB p95 > 800ms at 100% |\n| 9 | Post-login redirect-to-resume experiment arm | M | DEFERRED, provisional (taste T1) | Skips alerts the plan says members need; touches login flow; attribution needs its own arm | Final Approval Gate may flip to \"run concurrently\" |\n| 10 | Ranked \"next up\" card with live updates | XL | DEFERRED | New ranking + push infrastructure | After dashboard metric data at 100% |\n| 11 | ETag / short TTL cache on the endpoint | S | DEFERRED | Wait for p95 at 100% rollout | Dashboard owner checks endpoint p95 one week after 100% |\n| 12 | Token-only styling: PR review checklist item now, lint rule later | S | ACCEPTED (checklist) / DEFERRED (lint) | Keeps dark mode viable; lint needs design-system owner | Design-system owner adds lint rule |\n| 13 | Approach C: aggregate `GET /api/dashboard` with per-panel `PanelResult` envelope and `serverTime` | M | ACCEPTED, provisional (taste T2) | Only approach that gives per-panel error states in one round trip and a server clock for the read snapshot | Gate may choose B (client composes existing endpoints) |\n| 14 | Shared `PanelFrame` (state chrome) and shared `Toast` primitive | S | ACCEPTED, provisional (taste T4) | Three panels share one state switch; a11y policy requires a live region and no toast exists; shared placement is the same code in a reusable folder | Gate may choose inline status text over toast |\n| 15 | QuickActions as visual primary; fixed column order at sm/md/lg | S | ACCEPTED | Only panel that drives the metric; three equal cards is the generic pattern | \u2014 |\n| 16 | Keep confirmation modal for \"Mark all as read\" | S | ACCEPTED, provisional (taste T3) | No undo/restore API exists and the plan forbids new mutation APIs; confirm is the honest safety net | Gate may choose direct action + undo (needs a new mutation API, breaks a plan constraint) |\n\n## Accepted Scope (added to this plan)\n- Analytics baseline pull and find/do split before target lock (#1)\n- Numeric rollback triggers (#2)\n- Relative timestamps in ActivityFeed (#3)\n- Unread badge + title mirror (#4)\n- Back-to-previous-landing link during rollout (#5)\n- Action-pointing empty-state copy (#6)\n- Token-only styling as a PR review checklist item (#12)\n- Approach C: aggregate endpoint with per-panel `PanelResult` envelope and `serverTime` (#13)\n- Shared `PanelFrame` and shared `Toast` primitive (#14)\n- QuickActions as visual primary; fixed column order at sm/md/lg (#15)\n- Confirmation modal on the existing dialog primitive (#16)\n- Per-panel state machine and instrumentation (metrics, alerts, structured logs) as specified in the source plan's accepted CEO obligations\n\n## Deferred to TODOS.md\n- Keyboard shortcuts for quick actions (#7)\n- Prefetch dashboard payload during login redirect (#8)\n- Redirect-to-resume experiment arm under the same flag framework (#9, taste T1)\n- Ranked \"next up\" card + real-time push (#10)\n- Endpoint caching once p95 data exists (#11)\n- Token-only styling lint enforcement (#12)\n\n## Taste decisions (provisional; confirmed or overridden at the /autoplan Final Approval Gate)\n- T1: **Provisional decision: dashboard is the sole arm; experiment deferred (#9).** Alternative: run a redirect-to-resume arm concurrently.\n- T2: **Provisional decision: Approach C (#13).** Alternative: B, client composition of existing endpoints plus a new quick-actions endpoint.\n- T3: **Provisional decision: keep the confirmation modal (#16).** Alternative: direct action with undo, which needs a new mutation API.\n- T4: **Provisional decision: shared toast primitive (#14).** Alternative: inline status text per panel.\n", "hook": { "version": 1, "cwd": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-autoplan-chain-RnwL2i", "config": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/with-skills/.claude", "stateRoot": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/skill-home-zgCNxG/.gstack", "seenIds": [ "toolu_01BbKwZ7JFFdm2FLFdcNQXPq", "toolu_01W9d5F6LgadaG9o9maU8kaT", "toolu_01YXZEmM6gthBvGshzGE4zYb" ], "pending": { "source": "pre_tool_use", "sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e", "toolUseId": "toolu_01YXZEmM6gthBvGshzGE4zYb", "tool": "Edit", "file": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/skill-home-zgCNxG/.gstack/projects/gstack-autoplan-chain-RnwL2i/ceo-plans/2026-09-10-user-dashboard.md", "timestamp": "2026-09-10T09:42:01.132Z", "transcriptPath": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/with-skills/.claude/projects/-tmp-gstack-paid-shard-uaCq3n-tmp-gstack-autoplan-chain-RnwL2i/9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e.jsonl", "editDigest": { "version": 1, "beforeSHA256": "87ba6c184db192a85eb4fa0771dc59d6f3e12311f73a5bccc3497fd3138899ec", "requestSHA256": "0b2f9bd8a462279cfd3845b2200c448072a8efb68287b75aa95dcf0b38eb0efd", "oldLineHashes": [ "0e0dffc87c4190d31ee08629994526b6d12fb120ffb7a88ff1011d4f385f3923" ], "newLineHashes": [ "36e18b0e5881093948120902c19482cf1eafbf4716500ed739cabaa8064287e3" ] } }, "sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e" }, "publicTools": [ { "sessionId": "9b7779e9-18f9-4f8d-84ac-9e8bf4458a8e", "timestamp": "2026-09-10T09:36:51.205Z", "toolUseId": "toolu_01EMZc2Hum5xqWxGHi3dddtJ", "kind": "use", "name": "Edit", "input": { "replace_all": false, "file_path": "/tmp/gstack-paid-shard-uaCq3n/tmp/gstack-hermetic-2101964-HvDZyN/with-skills/.claude/plans/starry-riding-otter.md", "old_string": "## Review record\n", "new_string": "## Review record\n\n\n\n### Phase 0: Intake\n\n- SOURCE_PLAN: `.claude/plans/ui-heavy-feature.md` (sha256 2fdf0ece\u2026fddbc, 4607 bytes). Restore point written by `init`.\n- Context read: no CLAUDE.md, no TODOS.md, no design doc, one commit (`28730e6 init UI-heavy fixture`), no diff vs main, no stash, no TODO/FIXME markers.\n- **System audit key finding:** the repository contains only `README.md` and the plan file. None of the \"existing contracts\" the plan cites (repository methods, dialog primitive, action registry, HTTP clients, Vitest/RTL/Playwright, feature flags) exist in this repo. Every reuse claim is unverifiable here and is treated as a stated premise, not a checked fact.\n- Prerequisite /office-hours offer: auto-decided **skip** (P6, one-gate rule). Cross-project learnings config prompt: left unset (user preference, not a plan decision; LEARNINGS: 0 so no effect this run).\n- CLAUDE.md routing rules: user accepted (D1). Deferred until plan mode exits: write CLAUDE.md routing section and commit.\n- CEO methodology read log: `methodology.md` (2260 lines, sha256 cbb64d50\u20269a28) read at offsets 1/601/1201/1801, all four ranges successful through EOF.\n\n### Phase 1: CEO Review (SELECTIVE EXPANSION)\n\n**Mode selection (0F):** SELECTIVE EXPANSION per /autoplan override. Context default agrees: this is a feature on an existing system (consolidates three existing pages), not greenfield.\n\n**Landscape check:** Aside not installed, WebSearch not used in plan mode for this fixture. Proceeding with in-distribution knowledge. Layer 1 (tried and true): post-login \"home\" dashboards with a primary action rail, an alerts panel, and a recent-activity feed are the standard shape (Linear, GitHub, Notion, Asana home). Layer 2: the current trend is \"next up\" surfaces that rank one action above the fold rather than three equal panels. Layer 3 (first principles): the plan's metric is login-to-first-completed-task. Only QuickActions directly drives that metric; notifications and activity are context. Hierarchy should follow the metric.\n\n#### 0A. Premise Challenge\n\n| # | Premise | Stated or assumed | Assessment | Decision |\n|---|---------|-------------------|------------|----------|\n| P1 | Members spend a median 75s finding the next item after login | Stated, sourced from a team walkthrough, not the analytics the plan says already record login/action start/completion | Reasonable but weakly sourced. Real member data exists and is cheaper than a walkthrough. | Accept the problem; **add requirement**: pull real median/p90 login-to-first-task from existing analytics before locking the 45s target (auto-approved, in blast radius, <1h). |\n| P2 | A three-panel dashboard is the right shape to hit 45s | Assumed | Consolidation of three pages does move navigation time. But only one panel (QuickActions) drives task completion. A redirect-to-resume experiment could bank part of the win cheaper, though it skips alerts, which the plan says members need. | Accept dashboard shape. **Amend**: QuickActions is the visual primary. Redirect-to-resume experiment \u2192 **TASTE DECISION T1** (surfaced at gate) and deferred to TODOS.md. |\n| P3 | One aggregate `GET /api/dashboard` is better than the client calling existing endpoints | Assumed | No justification in plan. Quick actions have no existing list endpoint (registry + server predicates), so a new endpoint exists either way. | Resolved in 0C-bis: aggregate with per-panel result envelope (Approach C). B vs C close \u2192 **TASTE DECISION T2**. |\n| P4 | \"Mark all as read\" needs a confirmation modal | Stated | The bulk-read API is idempotent and snapshot-bounded, but there is no restore/undo API and the plan forbids new mutation APIs. Without undo, a confirm is the honest safety net. | Keep modal. Direct-action+undo would need a new mutation API (plan constraint). **TASTE DECISION T3** (recommend keep). |\n| P5 | A toast system is needed for action feedback | Stated | Accessibility policy requires a live region for nonblocking feedback; nothing exists. Building it as a one-page component would be regretted; building it as a shared primitive is the same code in a different folder. | Build toast as a shared UI primitive (P1, P4). Inline-text alternative \u2192 **TASTE DECISION T4** (recommend shared toast). |\n| P6 | No schema changes needed | Stated | Consistent with read composition + existing bulk-read API. | Accept. |\n| P7 | Existing fixtures/flags/metrics exist and are reusable | Stated | Cannot verify in this repo (see system audit). | Accept as premise; **flag at gate** as an unverified dependency, not a challenge. |\n\nNo premise is clearly wrong. No User Challenge queued from 0A.\n\n#### 0B. Existing Code Leverage Map\n\n| Sub-problem | Existing code (per plan) | Reused? |\n|---|---|---|\n| Auth + workspace scoping | Cookie sessions, membership middleware, request context member/workspace IDs | Yes: handler reads IDs from request context only |\n| Activity list | Repository list method (latest 20 + cursor, indexed) | Yes: called by aggregate handler |\n| Notifications list | Repository list method (latest 20 + cursor, indexed) | Yes |\n| Mark all as read | Member-scoped idempotent bulk-read API, snapshot-time bounded, CSRF | Yes: modal confirm calls it with server-issued snapshot time |\n| Quick actions | Action registry (3 actions, IDs, labels, routes, server eligibility predicates) | Yes: handler evaluates predicates, returns eligible set |\n| Typed client errors | Existing HTTP clients (unauthenticated/forbidden/validation/retryable/network) | Yes: each panel maps typed errors to states |\n| Dialog | Dialog primitive (focus trap, Escape, focus return) | Yes: modal composes it |\n| Toast | None | **New shared primitive** |\n| Layout tokens | Tailwind tokens, responsive page shell, buttons, links | Yes; **constraint added**: token-only values in dashboard components |\n| Tests | Vitest, RTL, Playwright, fixtures (member, other workspace, empty, failures) | Yes; dashboard specs new |\n| Rollout | Feature flags, request/error metrics | Yes; flag `dashboard_landing` |\n| Analytics | login, action start/completion, permission errors | Yes; add `dashboard_view`, `dashboard_panel_state`, `quick_action_click`, `mark_all_read` |\n\nNothing is rebuilt that already exists.\n\n#### 0C. Dream State\n\n```\n CURRENT STATE THIS PLAN 12-MONTH IDEAL\n Login \u2192 generic landing Login \u2192 /dashboard (flagged Login \u2192 \"next up\" home that\n Member visits 3 pages to cohort). One round trip, three ranks the single best action,\n resume / check alerts / panels, QuickActions primary, streams notification updates,\n inspect changes. ~75s (walk- per-panel failure isolation, personal layout, dark mode,\n through) to first task. ---> shared toast + modal, a11y, ---> cross-workspace view.\n No dashboard telemetry. telemetry on every state, Dashboard is the platform\n rollback = flag off. surface other teams add to.\n```\n**Dream state delta:** this plan lands the substrate (aggregate envelope, panel state machine, shared toast, instrumentation) that the ideal builds on. It does not do ranking, streaming, personalization, or dark mode. It moves toward the ideal; nothing here blocks it.\n\n#### 0C-bis. Implementation Alternatives\n\n```\nAPPROACH A: Minimal aggregate (all-or-nothing)\n Summary: GET /api/dashboard runs three repository calls; any failure \u2192 5xx; client shows one page-level error. Dialog primitive for confirm; inline status text instead of toast.\n Effort: S (human ~3 days / CC ~1h) Risk: Med (one slow panel blanks the page)\n Pros: fewest files; no new primitive; simplest handler\n Cons: violates \"error state for each panel\"; one repo timeout hides everything; no live region for feedback\n Reuses: repos, middleware, dialog, tokens\n Completeness: 5/10\n\nAPPROACH B: Client composes existing endpoints\n Summary: Page calls existing activity + notifications list endpoints in parallel plus a NEW /api/quick-actions endpoint. Panel isolation for free. Shared toast primitive.\n Effort: M (human ~5 days / CC ~1.5h) Risk: Low-Med (3 round trips on mobile; still one new endpoint)\n Pros: reuses two endpoints verbatim; per-panel failure isolation is structural; no partial-failure design\n Cons: 3 requests per landing on mobile; snapshot time for mark-all-read has no single server clock source; new endpoint needed anyway\n Reuses: existing list endpoints, clients, dialog\n Completeness: 8/10\n\nAPPROACH C: Aggregate with per-panel result envelope (RECOMMENDED)\n Summary: GET /api/dashboard runs three repository calls concurrently, returns\n { serverTime, activity: PanelResult, notifications: PanelResult, quickActions: PanelResult }\n where PanelResult = { status:\"ok\", data, cursor? } | { status:\"error\", code }.\n HTTP 200 whenever auth passes; per-panel errors are data. Shared toast primitive.\n Effort: M (human ~6 days / CC ~2h) Risk: Low\n Pros: one round trip; per-panel isolation matches the plan's own state requirement; serverTime gives the mark-all-read snapshot; envelope pattern is reusable\n Cons: new composition + partial-failure code; 200-with-errors needs its own metric (not HTTP status alerts)\n Reuses: repos, middleware, action registry, dialog, tokens, clients\n Completeness: 9/10\n```\n**RECOMMENDATION:** C, because it is the only approach that satisfies the plan's stated per-panel state requirement in one round trip and yields a server clock for the read snapshot (P1 completeness, P5 explicit). B is close (8/10) \u2192 **TASTE DECISION T2**.\n\n#### 0D. Mode-Specific Analysis (SELECTIVE EXPANSION)\n\n**Complexity check:** ~12 new files (page, 3 panels, hook, modal, toast primitive + provider, handler, envelope type, 3 test files, Playwright spec). Over the 8-file smell threshold, but each file is a distinct concern with no shared mutable state beyond the dashboard hook. Not reducible without merging panels into one component, which would defeat per-panel states. Accepted.\n\n**Minimum set achieving the goal:** page + QuickActions + endpoint + flag. Notifications and Activity could ship later without blocking the metric. Mechanical decision: do not reduce scope on a complete plan (P1). All three panels stay.\n\n**Expansion scan**\n- 10x: a ranked \"next up\" card above the panels, updated in real time, that turns the dashboard from a place you check into a place that tells you what to do. Concrete shape: server ranking over eligible actions + unread notifications, SSE channel for updates. Effort human ~3 weeks / CC ~1 day. **Deferred**: new infra (ranking service, push), outside blast radius.\n- Delight opportunities (\u22655), each decided by the blast-radius rule (in radius + <1d \u2192 accept; else defer):\n 1. Relative timestamps (\"3 min ago\") with absolute time on hover/focus in ActivityFeed. **ACCEPT** (1 file, <1h).\n 2. Unread count badge in NotificationsPanel header, mirrored into `document.title` while unread > 0. **ACCEPT** (1 file, <1h).\n 3. \"Back to the previous landing page\" link in the dashboard header during rollout, removed when flag reaches 100%. **ACCEPT** (1 file, <1h). Gives per-member escape hatch and a bounce-back metric.\n 4. Empty-state copy that points at the primary action (\"Nothing to resume. Create an item or invite a teammate.\") **ACCEPT** (copy only).\n 5. Keyboard shortcuts for the three quick actions. **DEFER** to TODOS.md: shortcut conflicts with existing pages need an audit; not on the metric path.\n 6. Prefetch `/api/dashboard` during the login redirect. **DEFER**: touches login flow, outside radius.\n 7. Post-login redirect-to-resume experiment arm under the same flag framework. **DEFER** + TASTE T1.\n- Platform potential: the `PanelResult` envelope and the toast primitive are reusable by any future composite page. Accepted as part of C.\n\n**Cherry-pick ceremony (auto-decided, neutral posture, logged in audit trail):** Accepted 1-4; deferred 5-7 and 10x.\n\n**Additional accepted hardening (from premise challenge and outside voice, all in blast radius):**\n- Baseline pull: real median/p90 login-to-first-task and find-vs-do split from existing analytics before locking 45s.\n- Numeric rollback triggers defined before rollout (see Section 9).\n- Token-only styling constraint for dashboard components (review checklist now; lint enforcement \u2192 TODOS.md).\n\n#### 0E. Temporal Interrogation (human hours; CC \u2248 10-20x faster)\n\n```\nHOUR 1 (foundations): PanelResult type + serverTime in the envelope; flag name dashboard_landing;\n route /dashboard gated by flag; toast primitive API (show({kind, message, persistent?})).\nHOUR 2-3 (core logic): Handler: Promise.allSettled over three repo calls; map rejections to {status:\"error\", code}\n by typed error class; never leak internal messages. Client hook maps 401\u2192login redirect,\n 403\u2192forbidden state, validation\u2192error state with log, retryable\u2192auto-retry once then\n error+Retry button, network\u2192error+Retry.\nHOUR 4-5 (integration): Mark-all-read uses envelope.serverTime as snapshot, POST via existing bulk-read API with\n CSRF; button disabled while in flight; on success optimistic clear + toast; on 401/403/CSRF\n failure toast \"Session expired, refresh\"; on retryable error retry once then persistent toast.\n Old landing page stays reachable at its route.\nHOUR 6+ (polish/tests): Skeleton fixed heights (no layout shift); reduced-motion disables skeleton shimmer and toast\n slide; toast max 3 stacked, 5s auto-dismiss, pause on hover/focus, role=\"status\" for\n success and role=\"alert\" for errors (persistent until dismissed); Playwright: flag on/off,\n each panel state, keyboard-only modal flow, screen reader names.\n```\nDecisions above are resolved now and recorded as accepted obligations.\n\n#### Section 1: Architecture Review\n\n```\n Browser (React) Server\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 /dashboard (flag: dashboard_landing) \u2502 \u2502 GET /api/dashboard \u2502\n \u2502 UserDashboard.tsx \u2502 1 fetch \u2502 DashboardHandler \u2502\n \u2502 \u251c\u2500 useDashboard() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6 \u2502 \u251c\u2500 session + membership middleware\u2502\n \u2502 \u251c\u2500 QuickActions (primary) \u2502 \u2502 \u251c\u2500 Promise.allSettled([ \u2502\n \u2502 \u251c\u2500 NotificationsPanel \u2502 \u2502 \u2502 activityRepo.list(ws, 20) \u2502\n \u2502 \u2502 \u2514\u2500 MarkAllReadModal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 POST \u2500\u2500\u2500\u25b6 \u2502 \u2502 notificationRepo.list(m,ws,20)\u2502\n \u2502 \u2502 (dialog primitive) \u2502 existing \u2502 \u2502 actionRegistry.eligible(m,ws)])\u2502\n \u2502 \u2514\u2500 ActivityFeed \u2502 bulk-read \u2502 \u2514\u2500 envelope {serverTime, 3\u00d7PanelResult}\n \u2502 ToastProvider (shared ui primitive) \u2502 \u2502 POST /api/notifications/read-all (existing)\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502 \u2502\n PostgreSQL PostgreSQL registry predicates\n```\n- **Data flow, four paths (GET /api/dashboard):** happy \u2192 200 envelope, all `ok`. Nil (no session) \u2192 middleware 401 before handler; client redirects to login. Empty (member with zero activity/notifications/eligible actions) \u2192 200, `ok` with `data: []`, each panel renders its empty state; QuickActions empty state must never occur in practice (create-item is always eligible for a member) but is still rendered and tested. Error (one repo throws) \u2192 that panel `{status:\"error\", code}`, others `ok`; page renders; metric emitted.\n- **State machine (per panel):** `idle \u2192 loading \u2192 (ok|empty|error) ; error \u2192 loading (retry)`. Invalid: `loading \u2192 loading` (prevented by in-flight guard), `ok \u2192 error` without a new request (impossible; state only changes on response).\n- **Mark-all-read state machine:** `closed \u2192 open (button) \u2192 submitting (confirm) \u2192 (closed+success toast | open+error toast)`. Double confirm prevented by disabling the confirm button while submitting. Escape during submitting is ignored (dialog stays until response) to avoid orphaned toasts.\n- **Coupling:** handler now depends on three repositories and the action registry; that fan-in is the point of the endpoint and is bounded by the envelope contract. No new coupling between panels; each consumes its own `PanelResult`.\n- **Scaling:** 10x load \u2192 three indexed LIMIT 20 queries per landing; DB connection use triples per request vs a single-page load. First to break: pool saturation under a login storm. Mitigation: concurrent calls share the request's pool budget; flag-gated cohort rollout observes p95. 100x \u2192 add short TTL cache per member (deferred, Section 7).\n- **SPOF:** PostgreSQL (existing). The endpoint adds none.\n- **Security architecture:** endpoint is read-only, scoped by request-context member/workspace; no IDs from query string. Mutation reuses existing CSRF-protected bulk-read API. See Section 3.\n- **Production failure scenario:** notifications table lock during a bulk job \u2192 `notificationRepo.list` times out \u2192 `notifications: {status:\"error\", code:\"retryable\"}`; page still shows actions and activity; panel shows Retry; `dashboard_panel_error{panel=notifications}` spikes \u2192 alert. Plan (as amended) accounts for it.\n- **Rollback:** flag `dashboard_landing` off \u2192 login lands on the previous page; under 1 minute; no migration; endpoint can stay deployed dark.\n- **Beauty / platform:** `PanelResult` as a typed discriminated union is the one abstraction; a new engineer reads it in 30 seconds. It makes any future composite page a two-line addition.\n\nFindings: (1) HTTP 200 with per-panel errors hides failures from status-code alerts \u2192 **decided**: emit `dashboard_panel_error` metric and alert on it (Section 8). (2) Old/new client mismatch during deploy \u2192 **decided**: page route gated by flag; endpoint deploys first (Section 9). Both auto-decided (mechanical, P5).\n\n#### Section 2: Error & Rescue Map\n\n```\n METHOD/CODEPATH | WHAT CAN GO WRONG | ERROR CLASS\n --------------------------------|-------------------------------------|---------------------------\n membership middleware | no/expired session | UnauthenticatedError (401)\n | member not in workspace | ForbiddenError (403)\n DashboardHandler | activityRepo.list timeout/DB error | RetryableServiceError\n | notificationRepo.list timeout/DB | RetryableServiceError\n | actionRegistry.eligible throws | RegistryEvaluationError\n | envelope serialization bug | TypeError (programmer)\n useDashboard (client) | network down | NetworkError\n | 401 | UnauthenticatedError\n | 403 | ForbiddenError\n | response fails schema check | ValidationError\n | 5xx / retryable | RetryableServiceError\n MarkAllRead submit | CSRF token stale | ForbiddenError (CSRF)\n | 401/403 | Unauthenticated/Forbidden\n | bulk-read API 5xx | RetryableServiceError\n | network | NetworkError\n | double submit | (prevented) in-flight guard\n ToastProvider | toast fires after unmount | (prevented) timer cleared on unmount\n | >3 toasts | (prevented) queue, oldest dropped\n\n ERROR CLASS | RESCUED? | RESCUE ACTION | USER SEES\n ----------------------------|----------|-------------------------------------------------|----------------------------------\n UnauthenticatedError | Y | client redirects to /login?next=/dashboard | login page\n ForbiddenError (endpoint) | Y | forbidden state, log warn with member/ws ids | \"You don't have access to this workspace\"\n RetryableServiceError (panel)| Y | handler maps to PanelResult error; client auto-retries once, then Retry button; log error with panel, ids, duration; metric | panel error state + Retry\n RegistryEvaluationError | Y | quickActions PanelResult error; log error | actions panel error + Retry\n TypeError (envelope) | Y | existing 500 handler; log with request id | page-level error + Retry (rare)\n ValidationError (client) | Y | error state, log to client error reporter | panel/page error + Retry\n NetworkError | Y | error state with Retry; no auto-retry | \"Can't reach the server\" + Retry\n ForbiddenError (CSRF) | Y | persistent error toast; keep modal open | \"Session expired. Refresh and try again.\"\n RetryableServiceError (POST)| Y | retry once, then persistent error toast | \"Couldn't mark all as read. Try again.\"\n NetworkError (POST) | Y | persistent error toast | same as above\n```\nNo catch-all rescues. Every rescued error logs member id, workspace id, request id, panel, and duration. No GAP rows. Finding: none unrescued. \"No issues, moving on\" after mapping 14 error paths.\n\n#### Section 3: Security & Threat Model\n\n| Threat | Likelihood | Impact | Mitigated? |\n|---|---|---|---|\n| IDOR: member reads another workspace's activity via query param | Low | High | Yes: handler takes IDs from request context only; test with \"another workspace\" fixture asserts 403/empty |\n| Ineligible action leaked to client | Med | Med | Yes: eligibility predicates evaluated server-side in handler; client never receives ineligible actions; clicking still hits existing server checks |\n| Open redirect via action route targets | Low | Med | Yes: routes come from the server registry, never from user input; client only renders registry routes |\n| XSS via activity descriptions / notification text | Med | High | Yes: render as text nodes, never `dangerouslySetInnerHTML`; RTL test asserts a `