Files
Project-SDE-WP-Suite/docs/waves/wave-4.md
n.siegfried 3d99d4b9d0 Import the R2 implementation spec into the repo
The plan was delivered as wp-suite-implementation-spec.zip and lived only in
Downloads, so every "read CLAUDE.md first" instruction in it pointed at a file
the repo did not have. Bring it in unchanged: CLAUDE.md, IMPLEMENTATION.md, and
docs/waves/wave-0 through wave-9 plus backlog.md.

UX-REVIEW-2026-08-14.md is committed alongside it. It is the review that
produced F1-F6, S1-S13 and the A/B/C assessments, and item IDs throughout the
wave files cite it, so it belongs under version control rather than sitting
untracked in the working tree.

No application code changes here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:42:56 -05:00

216 lines
8.6 KiB
Markdown

# Wave 4 — Platform prerequisites
**Items:** `B4`, `S3`, `S2`, `B5`, `S10`, `S11`, `S12`, part of `C4`
**Depends on:** wave 3 merged
**Why here:** `X1` and `X5` are blocking dependencies that resolve in this wave. Two of the
most valuable field requests — the QA gate (`CR-014`) and the kitting notification
(`CR-011`) — promise emails containing a direct link to a work package. Nothing in this app
has a URL. Until `T4.2` lands, those emails cannot be built.
`T4.5` through `T4.7` are small and could be done any time. They are here because every
component built in waves 5 through 8 inherits them, and retrofitting is what `C1` warns
about.
---
### T4.1 — B4: Server aggregates replace localStorage-derived counts
- **Items:** `B4`
- **Depends on:** T3.2
- **Blocks:** T5.3, T6.3, T6.5
- **Surface:** `server/` + `html/`
- **Files:** server API, launcher, creator dashboard
**Problem:** Launcher SOP status reads `localStorage`, and the creator's dashboard carries a
comment admitting it reads local data and wires to SQL in Phase 2. Per-browser numbers that
look authoritative are worse than no numbers: two people looking at the same project see
different counts and neither is told.
**Do:** Add server endpoints returning the aggregate counts the launcher and dashboard need,
computed from the database. Point both surfaces at them. Delete the `localStorage` reads for
anything presented as a count or a status rollup.
Design the aggregate shape to support grouping by building, floor and sector, because
`CR-018` needs exactly that in wave 6. You do not have to expose the grouping yet, but do not
build a shape that cannot.
**Do not:** leave a `localStorage` fallback for counts. A silently-stale fallback is the
failure mode being fixed. If the endpoint fails, show an error state.
**Done when:**
- [ ] launcher and dashboard counts come from the server
- [ ] two different browsers signed in as different users show identical counts for the same project
- [ ] no `localStorage` read remains behind any displayed count; grep confirms
- [ ] endpoint failure shows an explicit error state, not a zero and not a stale number
- [ ] the aggregate response can be grouped by location without a schema change
---
### T4.2 — S3: URL state
- **Items:** `S3`, part of `C4`
- **Depends on:** T2.2
- **Blocks:** T7.6, T8.3 (blocking dependency `X1`)
- **Surface:** `html/`
- **Files:** all 6 pages, routing/state handling
**Problem:** No `pushState` anywhere. Tab switches, dashboard views and package selections
change no URL. You cannot send someone a link to WP07, and a refresh loses your place.
**Do:** Give addressable state a URL. At minimum: the selected project, the selected work
package, the active tab or section, and the active dashboard view or filter. Restore state
from the URL on load. Make browser back and forward behave.
Deep links must work for a signed-out user: land on login, then continue to the requested
target after signing in. An email link that dumps the recipient on a dashboard is the
failure this task exists to prevent.
**Do not:** use hash-only routing if the server can serve paths. Do not put anything secret
in the URL.
**Done when:**
- [ ] a URL identifying a specific work package opens that work package directly
- [ ] the same URL works for a signed-out user via login, landing on the requested target
- [ ] refresh preserves project, package, tab and view
- [ ] browser back and forward move through states without a full reload or a broken view
- [ ] copying the URL and opening it in a second browser reaches the same view
- [ ] `pushState` count is greater than 0; record the new count
---
### T4.3 — S2: Autosave and unsaved-work guard
- **Items:** `S2`, part of `C4`
- **Depends on:** T4.2
- **Blocks:** T7.7 (blocking dependency `X8`)
- **Surface:** `server/` + `html/`
- **Files:** creator, SOP wizard, `work-package-suite-app.js:326`
**Problem:** No autosave and no unsaved-work guard. The only `beforeunload` listener is
analytics dwell tracking. On a form roughly 4,700px tall, a mis-click loses everything.
**Do:** Autosave drafts on a debounce and on step or section change. Add a real
`beforeunload` guard for genuinely unsaved changes, alongside the analytics listener rather
than replacing it. Add draft recovery: if a session ends with unsaved work, offer it back on
return.
**Do not:** fire the guard when nothing has changed. A dialog that appears on every exit
gets clicked through and is worse than none.
**Done when:**
- [ ] typing then navigating away and returning restores the work
- [ ] typing then closing the tab prompts; not typing then closing does not
- [ ] autosave survives a browser crash simulation (kill the tab, reopen)
- [ ] the analytics dwell listener still functions
- [ ] autosave failure is surfaced to the user, not swallowed
---
### T4.4 — B5: Honest save status
- **Items:** `B5`
- **Depends on:** T4.3
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** creator, SOP wizard, existing toast system
**Problem:** An "All changes saved" toast already appears, but it belongs to the sync outbox,
not the draft. The app already implies a promise it does not keep.
**Do:** A persistent save-state indicator tied to the actual draft: saving, saved with a
timestamp, or failed with a retry. Distinguish it clearly from the sync outbox message, or
rename the outbox message so the two are not confused.
**Done when:**
- [ ] the indicator reflects real draft state, verified by watching it during a save failure
- [ ] a failed save is visually distinct from a successful one and offers a retry
- [ ] the sync outbox message no longer reads as a draft-save confirmation
- [ ] the indicator is announced via `aria-live` (see `T4.5`)
---
### T4.5 — S10: `aria-live` regions
- **Items:** `S10`
- **Depends on:** T3.2
- **Blocks:** nothing, but every later task depends on the pattern existing
- **Surface:** `html/`
- **Files:** both toast systems, admin banners, `login.html` as the reference
**Problem:** Zero `aria-live` regions app-wide. Both toast systems and every admin banner
announce nothing. `login.html` already does this correctly with `role="alert"` and
`role="status"`; the pattern never spread.
**Do:** Copy the `login.html` pattern into both toast systems and the admin banners. Errors
use `role="alert"` (assertive), confirmations use `role="status"` (polite).
**Done when:**
- [ ] every toast and banner announces
- [ ] errors interrupt, confirmations do not
- [ ] verified with a screen reader, or with an automated check plus a manual read of the markup
- [ ] `aria-live` region count is greater than 0; record it
---
### T4.6 — S11: Helper text contrast
- **Items:** `S11`
- **Depends on:** T3.2
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** canonical tokens, `console.css:85-87` as the reference
**Problem:** `--text-dim: #8d8d8d` at 12px is about 2.9:1. `console.css:85-87` already
diagnosed and fixed this; the other two sheets did not follow. Now that tokens are
consolidated, fix it once.
**Do:** Raise the dim-text token to meet 4.5:1 against its actual backgrounds. Check it on
every surface it lands on, including shaded cards and table rows, not just white.
**Done when:**
- [ ] all helper and hint text measures at least 4.5:1 against its real background
- [ ] the fix lives in the canonical token, not per-sheet
- [ ] `console.css` no longer needs its local override; remove it
- [ ] measured values are recorded in the PR
---
### T4.7 — S12: Restore the focus ring
- **Items:** `S12`
- **Depends on:** T3.2
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** `work-package-suite-styles.css:322-328`
**Problem:** `outline: none` plus a pale 3px glow. Keyboard users cannot see where they are.
**Do:** A visible focus indicator meeting at least 3:1 against adjacent colors, applied
consistently to every interactive element. Use `:focus-visible` so mouse users do not see
rings on click.
**Do not:** ship `outline: none` without a replacement anywhere in the app.
**Done when:**
- [ ] tabbing through every page shows a clearly visible indicator at each stop
- [ ] the indicator meets 3:1 against adjacent colors on every background it appears on
- [ ] no `outline: none` remains without an accompanying visible replacement; grep confirms
- [ ] mouse clicks do not leave a persistent ring
---
## Wave 4 exit criteria
- [ ] counts come from the server and match across browsers
- [ ] work packages are addressable by URL, including from a signed-out state
- [ ] drafts autosave and unsaved work is guarded
- [ ] toasts announce, helper text is legible, focus is visible
- [ ] `X1` and `X5` are cleared, unblocking `CR-011`, `CR-014`, `CR-004` and `CR-018`