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

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 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