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>
216 lines
8.6 KiB
Markdown
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`
|