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>
8.6 KiB
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
localStorageread 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 ofC4 - 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
pushStatecount is greater than 0; record the new count
T4.3 — S2: Autosave and unsaved-work guard
- Items:
S2, part ofC4 - 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(seeT4.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.htmlas 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-liveregion 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-87as 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.cssno 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: noneremains 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
X1andX5are cleared, unblockingCR-011,CR-014,CR-004andCR-018