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>
This commit is contained in:
2026-08-14 17:42:56 -05:00
parent 9459e76a6c
commit 3d99d4b9d0
14 changed files with 2254 additions and 0 deletions

53
docs/waves/backlog.md Normal file
View File

@@ -0,0 +1,53 @@
# Backlog
Anything noticed during implementation that is real but not in the plan goes here instead of
into the current PR. `CLAUDE.md` requires this: every change traces to an item ID, so
unplanned work gets logged rather than built.
Add an entry, do not fix it inline. This file is reviewed at `T9.7` and feeds the next spec
revision.
## Format
```markdown
### BL-001 — Short title
- **Found during:** T3.2
- **Where:** path/to/file.js:120
- **What:** one or two sentences on the problem
- **Why not now:** out of scope for the current wave / needs a product decision / larger than the task
- **Suggested wave or follow-up:** wave 9 / next revision / needs Nick
```
## Known follow-ups already identified in the spec
These are logged from the source documents, not discovered in code. They are real but
deliberately deferred.
### BL-000a — Validated P6 activity lookup
- **From:** `CR-001`
- **What:** `CR-001` accepts free text for the P6 Activity ID. A validated lookup against an imported P6 activity list was identified as the eventual want.
- **Why not now:** the Micron schedule is actively being reworked, so importing an activity list now would import churn.
- **Suggested:** next revision, once the schedule stabilizes.
### BL-000b — Field-level toggles in General Information
- **From:** `CR-006`
- **What:** `CR-006` toggles whole sections. General Information may need per-field toggles, since projects differ in which identifiers they use.
- **Why not now:** section-level toggles cover every removal request currently on the list.
- **Suggested:** next revision, if a second project needs a different field set.
### BL-000c — Estimated versus actual hours productivity factor
- **From:** `CR-017`
- **What:** Actual Hours is retained and rolls up. Comparing it against estimated hours would produce a productivity factor, which was the stated reason for wanting the field.
- **Why not now:** estimated hours capture is not in scope this round.
- **Suggested:** next revision.
### BL-000d — Attachment merge versus list on export
- **From:** `CR-008` / `T9.1`
- **What:** whether the PDF export merges attachments into one package or lists them separately.
- **Why not now:** product decision, raised in the `T9.1` PR.
- **Suggested:** needs Nick.

91
docs/waves/wave-0.md Normal file
View File

@@ -0,0 +1,91 @@
# Wave 0 — Orientation
**Do this before wave 1.** Nothing else in the plan should be started until `T0.1` is merged.
The item IDs and file references throughout this plan come from a UX review conducted on
branch `users/directory-super-user`, not from a fresh read of the repo you are in. The line
numbers were accurate at the time of the review. Confirm them before relying on them.
---
### T0.1 — Build the file map
- **Items:** none (prerequisite)
- **Depends on:** nothing
- **Blocks:** every other task
- **Surface:** documentation only
- **Files:** creates `docs/reference/file-map.md`
**Do:**
Inventory the frontend and record it. The review described `html/` as 6 pages, 4
stylesheets, roughly 11,900 lines. Confirm that and write down what is actually there.
For each of the 6 pages record: file path, what it is called in this plan (launcher, SOP
wizard, creator, admin, field view, directory/users, login), which stylesheets it loads,
which scripts it loads, and whether it renders inside an iframe or hosts one.
For each stylesheet record: path, and which pages consume it.
Then verify these specific references from the review and record the current line number
next to each, or mark it `NOT FOUND`:
| Reference | Claim |
|---|---|
| `wp-creation-app.js:1144` | `alert()` reading "Subject and WP Type are required" |
| `wp-creation-app.js:1962-1972` | logged-override path for predecessor constraints |
| `work-package-suite-app.js:326` | the only `beforeunload` listener, analytics dwell tracking |
| `work-package-suite-styles.css:322-328` | `outline: none` plus a pale 3px focus glow |
| `work-package-suite-styles.css:336` | comment about `.field-hint` rendering unstyled |
| `console.css:85-87` | the contrast fix that was not propagated |
| `admin.js:484-517` | language and time localization |
| `server/seed_demo.py` | seeding script that does not authenticate |
Also record counts, so later waves can prove they went down:
- occurrences of `alert(`, `confirm(`, `prompt(` — review found 79 app-wide, 43 in the creator
- `<div` and `<span` elements carrying `onclick` — review found 12 and 2
- declarations of `#0f62fe` — review found 4
- `.help-tip` elements — review found 15
- `aria-live` regions — review found 0
- `pushState` calls — review found 0
**Do not:** change any application code in this task. It is a read and a document.
**Done when:**
- [ ] `docs/reference/file-map.md` exists and lists all pages, stylesheets and scripts
- [ ] every reference in the table above is confirmed with a current line number or marked `NOT FOUND`
- [ ] the seven counts are recorded with the command used to produce each
- [ ] any file path used in a later wave file that does not exist is listed under a `Discrepancies` heading
---
### T0.2 — Confirm the app runs and the baseline is captured
- **Items:** none (prerequisite)
- **Depends on:** T0.1
- **Blocks:** every task with a visual done-when
- **Surface:** local environment
- **Files:** creates `docs/reference/baseline/`
**Do:**
Run the app locally (`uvicorn server.app:app` against a throwaway SQLite database) and drive
the first-run flow: sign in, create a project, SOP wizard, creator, directory, field view.
Capture baseline screenshots at **390px** and **1440px** for each of the 6 pages into
`docs/reference/baseline/`. Name them `<page>-<width>.png`. These are the before images
every later PR compares against.
While doing this, confirm each of `F1` through `F6` still reproduces and note it. If one does
not reproduce, say so in the PR rather than deleting the task from wave 1.
**Do not:** fix anything you see. Wave 1 does that.
**Done when:**
- [ ] the app runs locally from a clean database
- [ ] 12 baseline screenshots exist (6 pages x 2 widths)
- [ ] each of `F1``F6` is marked reproduces / does not reproduce, with a screenshot reference
- [ ] the exact run command and any setup steps are recorded in `docs/reference/file-map.md`

185
docs/waves/wave-1.md Normal file
View File

@@ -0,0 +1,185 @@
# Wave 1 — Rendering defects and tooling
**Items:** `F1`, `F2` (interim), `F3`, `F4`, `F5` (interim), `S13`
**Depends on:** wave 0
**Why first:** none of these need a design decision, and `F1` and `F2` block all chrome work
in wave 2.
Two tasks here are deliberately partial. `F2` gets a containment fix only because `B1` in
wave 2 resolves it properly, and `F5` gets a one-line fix because `C3` in wave 3 removes the
underlying cause. Do not over-build either.
---
### T1.1 — F1: Header project switcher is stale on every page
- **Items:** `F1`
- **Depends on:** T0.1
- **Blocks:** T2.1, T2.2
- **Surface:** `html/`
- **Files:** per file-map — the app bar/header component and whatever holds active-project state
**Problem:** The hero, the project picker and the create-user card all show the active
project. The app bar still reads "Select a project". Two sources of truth for the same value,
and the global one loses.
**Do:**
Find every place the active project is read or stored. There is more than one. Collapse them
to a single source, and have the app bar subscribe to it rather than holding its own copy.
Set the value on project selection and on page load, including a hard refresh and a
deep-linked page.
**Do not:** introduce a state library. This is a single value with a small number of readers.
**Done when:**
- [ ] the app bar shows the active project on all 6 pages
- [ ] value survives a hard refresh on every page
- [ ] switching projects updates the app bar and the hero in the same interaction, with no reload
- [ ] with no project selected, the app bar reads "Select a project" and the picker is reachable
- [ ] exactly one code path writes active-project state; note its location in the PR
---
### T1.2 — F2 (interim): stop the app bar clipping at 390px
- **Items:** `F2` partial
- **Depends on:** T1.1
- **Blocks:** nothing (superseded by T2.2)
- **Surface:** `html/`
- **Files:** app bar styles
**Problem:** At 390px the nav wraps to three rows and clips. "Sign out" is cut in half and
search is truncated. Field View runs at this width on tablets with gloved hands.
**Do:** The smallest change that guarantees no control is unreachable or visually cut. Allow
the bar to scroll or stack rather than clip. This is triage, not a redesign — `T2.2` deletes
this markup.
**Do not:** build a responsive menu, a hamburger, or an avatar dropdown. Wave 2 ships the
existing drawer instead, and anything built here would be thrown away.
**Done when:**
- [ ] at 390px no nav control is clipped or unreachable on any of the 6 pages
- [ ] "Sign out" is fully visible and tappable at 390px
- [ ] search is either fully usable or deliberately hidden at this width, not truncated
- [ ] no layout change at 1440px — screenshot diff against baseline is empty
- [ ] the PR states this is interim and names `T2.2` as the real fix
---
### T1.3 — F3: Logo and project name collide in the SOP header
- **Items:** `F3`
- **Depends on:** T1.1
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** SOP wizard page and its header styles, `.header-left`
**Problem:** `.header-left` and the injected chrome compete for the same run of the bar and
overlap.
**Do:** Give the two elements a defined relationship rather than letting both claim the same
space. Verify with a long project name — "Micron EUV Cleanroom Enable 2667008" is the real
one and it is what breaks things.
**Do not:** solve it by truncating the project name. `B2` in wave 2 owns truncation and
needs a breakpoint plan, not a silent ellipsis.
**Done when:**
- [ ] no overlap at 390px, 768px, 1024px and 1440px
- [ ] tested with the longest real project name
- [ ] the logo remains fully visible at all four widths
---
### T1.4 — F4: Comments drawer renders off-screen over the header
- **Items:** `F4`
- **Depends on:** T0.1
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** standalone creator page and comments drawer styles
**Problem:** In the standalone creator, the comments drawer renders off-screen and over the
header.
**Do:** Fix the positioning context and stacking. Check whether the drawer is positioned
relative to the iframe or the parent — if the iframe boundary is implicated, note it in the
PR because `T7.1` removes that boundary.
**Do not:** raise `z-index` until something happens to work. Identify the containing block
first and say what it was in the PR.
**Done when:**
- [ ] the drawer opens fully on screen in the standalone creator at 390px and 1440px
- [ ] the drawer does not cover the header
- [ ] the drawer closes and reopens correctly, and scroll position is not lost
- [ ] behavior is identical in the embedded and standalone creator
---
### T1.5 — F5 (interim): SOP wizard fields read as read-only
- **Items:** `F5` partial, `A3` partial
- **Depends on:** T0.1
- **Blocks:** nothing (completed by T3.4)
- **Surface:** `html/`
- **Files:** SOP wizard stylesheet
**Problem:** Wizard inputs render with a `#f4f4f4` fill and `#e0e0e0` border, which reads as
disabled. Users do not type in them. The cause is that the wizard redeclares its own tokens
and never sees `--cds-field: #ffffff`.
**Do:** Make the fields look editable. The minimal correct fix is to have the wizard consume
the existing field token instead of its local override.
**Do not:** start token consolidation here. That is `T3.1``T3.3` and it is a separate,
mechanical, whole-app change. Fix the field appearance only.
**Done when:**
- [ ] wizard inputs render with a white fill and a border visually consistent with inputs elsewhere
- [ ] genuinely disabled inputs still look disabled and are distinguishable from enabled ones
- [ ] no other component changes appearance — screenshot diff limited to the wizard
- [ ] the PR notes that `T3.4` removes the underlying duplicate token
---
### T1.6 — S13: `server/seed_demo.py` cannot seed a running instance
- **Items:** `S13`
- **Depends on:** T0.1
- **Blocks:** nothing, but makes every later task's setup faster
- **Surface:** `server/`
- **Files:** `server/seed_demo.py`, and the smoke test for reference
**Problem:** Every `/api/` route requires a session and the script sends none, so it cannot
seed a running instance. It predates the commit that taught the smoke test to sign in.
**Do:** Give the script the same authentication the smoke test uses. Reuse that code path
rather than writing a second one.
**Do not:** add a bypass, a debug flag, or an unauthenticated seeding route to the server.
The script authenticates like a client; the server does not get weaker.
**Done when:**
- [ ] `seed_demo.py` runs against a freshly started instance and exits 0
- [ ] the seeded data is visible in the UI after running it
- [ ] running it twice does not error or duplicate data, or it fails with a clear message
- [ ] no new unauthenticated route exists — confirm by diff
---
## Wave 1 exit criteria
- [ ] `F1`, `F3`, `F4` fully resolved and confirmed against the wave 0 baseline screenshots
- [ ] `F2` and `F5` contained, with their real fixes referenced (`T2.2`, `T3.4`)
- [ ] `S13` fixed and seeding works
- [ ] no new `<div onclick>`, no new raw hex values, no new `alert()` calls introduced
- [ ] `F6` untouched — it is a structural problem fixed by section tabs in `T7.2`

108
docs/waves/wave-2.md Normal file
View File

@@ -0,0 +1,108 @@
# Wave 2 — Drawer everywhere, flat strip deleted
**Items:** `B1`, `B2`, `S4`, `F2` (full)
**Depends on:** wave 1 merged
**Why here:** this settles global navigation before anything else is redrawn, and it resolves
the 390px collapse properly.
**The correction that defines this wave.** The proposed UX change list called for building a
new avatar menu to collapse the eight-item nav. Do not build it. The app already has a
drawer that holds the entire nav, and the review's judgment was that it is the best chrome in
the app. It ships on 3 of 6 pages while a flat strip duplicates it. Shipping the existing
drawer everywhere and deleting the strip is less work than building the menu, and it fixes
`F2` as a side effect.
---
### T2.1 — Extract the drawer into a shared component
- **Items:** `B1` part 1
- **Depends on:** T1.1
- **Blocks:** T2.2
- **Surface:** `html/`
- **Files:** the drawer markup/styles/script currently used by admin, field view and directory
**Do:** Lift the drawer into one shared implementation the pages include, rather than three
near-copies. Keep its current behavior and appearance — this is extraction, not redesign.
Where the three copies differ, take the admin version unless the PR explains otherwise.
**Do not:** change the drawer's visual design, its animation, or its contents in this task.
**Done when:**
- [ ] one drawer implementation exists; no page defines its own
- [ ] admin, field view and directory look and behave identically to the wave 0 baseline
- [ ] screenshot diff against baseline for those three pages is empty at both widths
- [ ] the drawer reads active-project state from the single source established in `T1.1`
---
### T2.2 — Mount the drawer on all six pages and delete the flat strip
- **Items:** `B1` part 2, `S4`, `F2` full
- **Depends on:** T2.1
- **Blocks:** T5.1, T7.2
- **Surface:** `html/`
- **Files:** all 6 pages, the flat nav strip markup and styles
**Problem:** The SOP wizard has its own header and no drawer. The creator has neither. These
are the two pages people actually live in, so global nav is missing exactly where it is most
needed (`S4`).
**Do:** Mount the shared drawer on all six pages, including the SOP wizard and the creator.
Delete the flat nav strip and its styles entirely. Reconcile the SOP wizard's bespoke header
against the shared chrome — the wizard keeps its step context, it does not keep its own
global nav.
Unify the brand treatment while you are here: the creator uses a mono wordmark and everything
else uses a logo chip. Pick the logo chip.
**Do not:** leave the flat strip in place behind a media query "just in case". Delete it. Its
existence is what produced the duplication.
**Done when:**
- [ ] all 6 pages render the same drawer with the same contents
- [ ] the flat nav strip is deleted; grep confirms no remaining references
- [ ] at 390px on every page, no nav control is clipped and every destination is reachable
- [ ] Field View at 390px passes with tap targets of at least 44px
- [ ] the creator and the SOP wizard show the same brand treatment as the other four pages
- [ ] the interim fix from `T1.2` is removed rather than layered on top
---
### T2.3 — B2: Project switcher truncation and breakpoints
- **Items:** `B2`
- **Depends on:** T2.2
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** app bar / drawer header styles
**Problem:** "Micron EUV Cleanroom Enable 2667008" does not fit 280px, and existing rules
narrow the switcher to 150px below 900px. This needs a breakpoint plan, not a width bump.
**Do:** Decide what the switcher shows at each breakpoint and implement that. A workable
plan: full name where it fits, project code alone at narrow widths, full name always
available in the drawer or on hover/focus. Whatever you choose, write it into
`docs/reference/file-map.md` so it is not re-litigated.
**Do not:** apply a plain ellipsis at every width. A project name truncated to "Micron EUV
Clean..." on the page where someone confirms which project they are in is the failure this
item exists to prevent.
**Done when:**
- [ ] the active project is identifiable at 390px, 768px, 1024px and 1440px
- [ ] the full name is reachable at every width by some means, and that means is documented
- [ ] correctness from `T1.1` is preserved — the switcher never reads "Select a project" while a project is active
- [ ] tested with the longest real project name
---
## Wave 2 exit criteria
- [ ] one drawer, six pages, no flat strip
- [ ] `F2` resolved for real; the `T1.2` interim is gone
- [ ] `S4` closed: global nav exists on the SOP wizard and the creator
- [ ] no visual regression on admin, field view or directory versus baseline

147
docs/waves/wave-3.md Normal file
View File

@@ -0,0 +1,147 @@
# Wave 3 — Token consolidation and one button system
**Items:** `C3`, `S5`, `A3`/`F5` (full), `A5`
**Depends on:** wave 2 merged
**Why here:** every remaining visual task lands once instead of four times. Skipping this
means "one button system" gets implemented in four token systems and drifts, which is exactly
how `.field-hint` broke.
This wave should produce **no intentional visual change** except the two known defects it
fixes. Treat any other diff in the screenshot comparison as a bug in your refactor.
---
### T3.1 — Inventory the token systems
- **Items:** `C3` part 1, `S5` part 1
- **Depends on:** T2.3
- **Blocks:** T3.2
- **Surface:** documentation
- **Files:** creates `docs/reference/tokens.md`
**Problem:** Four parallel token systems. `#0f62fe` alone is declared four times, as
`--cds-interactive-01`, `--primary`, and `--accent` twice.
**Do:** Produce a mapping table: every custom property and every raw hex, hsl or rgb value in
the four stylesheets, where it is declared, what consumes it, and which canonical token it
maps to. Include spacing and type scales, not just color. Mark genuine duplicates,
near-duplicates (values one or two hex digits apart, which are usually accidents), and
one-offs.
**Do not:** change any stylesheet in this task.
**Done when:**
- [ ] `docs/reference/tokens.md` lists every declaration with file and line
- [ ] every value is mapped to a proposed canonical token or flagged as a one-off with a reason
- [ ] near-duplicates are called out explicitly, since those are where unintended visual change will come from
- [ ] the `#0f62fe` count matches or corrects the wave 0 baseline count
---
### T3.2 — Establish one source of truth
- **Items:** `C3` part 2, `S5` part 2
- **Depends on:** T3.1
- **Blocks:** T3.3, T3.4, T4.6, T4.7
- **Surface:** `html/`
- **Files:** all 4 stylesheets
**Do:** Create the canonical token set in one place. Convert the other stylesheets to alias
it. Page stylesheets declare no new values. Keep existing custom property names as aliases
where deleting them would touch large amounts of markup — an alias pointing at the canonical
token is fine; a second declaration of the same literal is not.
**Do not:** change any rendered value in this task, including the two known defects. Fix
those in `T3.3` and `T3.4` so their diffs are reviewable separately from a 4-stylesheet
refactor.
**Done when:**
- [ ] exactly one declaration exists per canonical value; grep for `#0f62fe` returns 1
- [ ] no page stylesheet declares a raw color, spacing or type value
- [ ] screenshot diff against the wave 2 state is empty on all 6 pages at both widths
- [ ] `docs/reference/tokens.md` is updated to reflect what was actually built
---
### T3.3 — Fix `.field-hint` and the drift it documents
- **Items:** `S5` part 3
- **Depends on:** T3.2
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** `work-package-suite-styles.css` around line 336
**Problem:** There is a comment at `work-package-suite-styles.css:336` describing
`.field-hint` rendering unstyled. It is the documented consequence of the duplicate token
systems.
**Do:** Fix `.field-hint` properly now that the tokens are consolidated, and delete the
comment. Then check for the same class of failure elsewhere: any selector that silently
renders unstyled because it referenced a token the page never saw.
**Done when:**
- [ ] `.field-hint` renders correctly everywhere it appears
- [ ] the stale comment is deleted
- [ ] any other silently-unstyled selectors found are listed in the PR and either fixed or logged
---
### T3.4 — A3/F5 full fix: fields look editable
- **Items:** `A3`, `F5` full
- **Depends on:** T3.2
- **Blocks:** T5.1
- **Surface:** `html/`
- **Files:** SOP wizard stylesheet
**Do:** Remove the wizard's local field token override entirely, so it consumes the canonical
field token. Remove the interim fix from `T1.5` rather than leaving both.
**Done when:**
- [ ] the wizard declares no field-specific color of its own
- [ ] enabled inputs are visually identical to enabled inputs on every other page
- [ ] disabled inputs remain clearly distinguishable from enabled ones
- [ ] the `T1.5` interim change is gone; grep confirms it
---
### T3.5 — A5: one button system
- **Items:** `A5`
- **Depends on:** T3.2
- **Blocks:** T5.1, T7.2
- **Surface:** `html/`
- **Files:** button styles, and any page overriding them
**Problem:** Green `Save & View` and green `SOP Complete` against blue everywhere else. Two
button languages competing.
**Do:** One button system: primary, secondary, tertiary, destructive. Map every existing
button to one of those roles. Green becomes a status color, not an action color.
Apply sentence case **to buttons and field labels only**.
**Do not:** touch the uppercase card-header idiom in `console.css`. It is deliberate, it
works, and the review specifically scoped the sentence-case rule to exclude it. Changing it
is out of scope.
**Done when:**
- [ ] every button in the app maps to one of four documented roles
- [ ] no green action buttons remain; `Save & View` and `SOP Complete` use the primary role
- [ ] buttons and field labels are sentence case
- [ ] uppercase card headers in `console.css` are unchanged — confirm by diff
- [ ] button roles are documented in `docs/reference/tokens.md`
---
## Wave 3 exit criteria
- [ ] one token source of truth; page sheets alias only
- [ ] `#0f62fe` declared once
- [ ] no unintended visual change on any page versus wave 2
- [ ] fields look editable, buttons speak one language

215
docs/waves/wave-4.md Normal file
View File

@@ -0,0 +1,215 @@
# 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`

239
docs/waves/wave-5.md Normal file
View File

@@ -0,0 +1,239 @@
# Wave 5 — Launcher and SOP wizard
**Items:** `A4`, `B3`, `S9`, `S1` (wizard), `CR-005`, `CR-006`, `CR-002`, `CR-016`
**Depends on:** wave 4 merged
**Why here:** `CR-006` is the enabler for every removal request in the plan. Building it
before `CR-002` and `CR-016` means nothing gets deleted that another project may need.
First field-visible wave. Everything before this was prerequisites.
---
### T5.1 — A4 + S9: Vertical stepper with real buttons
- **Items:** `A4`, `S9`
- **Depends on:** T3.4, T3.5, T2.2
- **Blocks:** T5.4, T5.5
- **Surface:** `html/`
- **Files:** SOP wizard step rail
**Problem:** The step rail is 10 `<div>` elements with `onclick`, so it is unreachable by
keyboard. There is also an orphaned "1 / 10" counter doing the job a stepper should do.
**Do:** Rebuild the rail as a vertical stepper showing all 10 steps, which is complete, which
is current, and which are not yet reachable. Every step is a real `<button>`. Retire the
"1 / 10" counter.
Per `C1`, this is the moment to make it accessible — it is being rebuilt anyway.
**Do not:** ship a single `<div onclick>` in the replacement. The app-wide count from wave 0
must go **down** by 10.
**Done when:**
- [ ] all 10 steps are `<button>` elements
- [ ] the rail is fully operable by keyboard: tab, arrow keys, enter and space
- [ ] current step is exposed with `aria-current`
- [ ] completed, current and unavailable states are distinguishable without relying on color alone
- [ ] the "1 / 10" counter is gone
- [ ] app-wide `<div onclick>` count dropped by 10 versus the wave 0 baseline
---
### T5.2 — B3: Launcher empty state, then remove the picker card
- **Items:** `B3`
- **Depends on:** T4.1
- **Blocks:** T5.3
- **Surface:** `html/`
- **Files:** launcher, project picker card
**Problem:** The proposal removes the project-picker card, but the first-run empty state
lives inside it. A header dropdown is the wrong home for "create your first project".
**Do:** Build an explicit first-run empty state on the launcher: no projects yet, here is how
to create one, here is the sample project. Only once that exists, remove the picker card.
Order matters — removing the card first strands new users.
**Done when:**
- [ ] a brand-new account with zero projects sees a clear path to create one
- [ ] the sample project remains discoverable from the empty state
- [ ] the picker card is removed only after the empty state ships
- [ ] switching projects still works from the header for users who have projects
---
### T5.3 — B4 surface: pipeline strip
- **Items:** `B4` UI half
- **Depends on:** T4.1, T5.2
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** launcher
**Do:** Build the four-cell pipeline strip against the server aggregates from `T4.1`. Each
cell links through to the filtered list, using the URL state from `T4.2`.
**Do not:** ship it against `localStorage`. That is the specific thing `B4` warns about.
**Done when:**
- [ ] every number comes from a server endpoint
- [ ] each cell links to a filtered view via a shareable URL
- [ ] a project with zero work packages renders a sensible empty state, not four zeros that look broken
- [ ] the strip announces updates via `aria-live` if it refreshes in place
---
### T5.4 — CR-005: Upload building / floor / sector lists at SOP setup
- **Items:** `CR-005`
- **Depends on:** T5.1
- **Blocks:** T6.3
- **Surface:** `server/` + `html/`
- **Files:** SOP wizard, server API, schema
**Intent:** The location taxonomy differs per project. On Micron, floors within B100 behave
like separate buildings, so floor and sector are the unit of both execution and cost
tracking. Configure it once per SOP rather than hard-coding it.
**Do:** A wizard step accepting a bulk upload (CSV or paste) of Building / Floor / Sector
values, with the hierarchy preserved. Editable after import: add, rename, deactivate. Store
as structured codes, not display strings — `CR-018` needs to group by them.
Deactivating a value hides it from new work packages without breaking existing ones that
reference it.
**Do not:** hardcode floor or building names. The B100 list has not been supplied yet (see
`IMPLEMENTATION.md` section 8). Seed with obviously-fake sample values.
**Done when:**
- [ ] CSV upload and paste both work and report rejected rows with reasons
- [ ] duplicates are detected and reported rather than silently merged
- [ ] values are editable after import
- [ ] deactivating a value hides it from new work packages; an existing work package referencing it still renders
- [ ] values are stored as codes suitable for grouping
- [ ] no guessed real-world floor names exist anywhere in the code
---
### T5.5 — CR-006: Section toggles
- **Items:** `CR-006`
- **Depends on:** T5.1
- **Blocks:** T5.6, T5.7, T7.2
- **Surface:** `server/` + `html/`
- **Files:** SOP wizard, work package renderer, PDF export, creator
**Intent:** This is the structural fix behind most removal requests in the plan. Rather than
deleting fields globally, each project turns on only the sections it uses. It is what lets
Micron drop Kitting and Assets while another project keeps them.
**Do:** In the SOP creation tool, list every work package section with an on/off toggle:
General Information, Location, Scope of Work, Assets, Materials, Kitting, Drawings and
Attachments, Constraints, QA/QC, Closeout. Default all on for a new SOP.
A section toggled off does not render in the creation form, the detail view, or the PDF
export. Data already captured is retained, and toggling back on restores it intact.
**Do not:** delete data on toggle-off. Ever. And note `X4`: the creator is an iframe child
until `T7.1`, so toggle propagation into the creator may need revisiting in wave 7 — test it
and say what you found.
**Done when:**
- [ ] all 10 sections are toggleable
- [ ] a section toggled off is absent from the form, the detail view and the PDF export
- [ ] toggling off then on restores prior data with no loss
- [ ] new SOPs default to all sections on
- [ ] toggle state propagates into the creator, or the PR documents exactly where it does not and why
---
### T5.6 — CR-002: Remove Acumatica Cost Code and Task
- **Items:** `CR-002`
- **Depends on:** T5.5
- **Blocks:** nothing
- **Surface:** `html/` + `server/`
- **Files:** work package form, detail view, PDF export
**Intent:** The team concluded these are noise on a field work package. Cost codes are
effectively constant and the Acumatica task mapping is a PM concern. The cost visibility they
actually want is by building and floor, which is `CR-004` and `CR-018`.
**Do:** Hide both fields from the form, the detail view and the PDF export, using the section
or field toggle mechanism from `T5.5`. Retain the data and the model.
**Do not:** drop the columns or delete the data. Another project may need these fields, and
that is what the toggles are for.
**Done when:**
- [ ] neither field appears in the form, detail view or PDF export for the Micron EUV SOP
- [ ] existing records still hold their values; confirm by querying
- [ ] the fields can be re-enabled for another SOP without a code change
- [ ] no schema migration drops data
---
### T5.7 — CR-016: Assets section off for Micron EUV
- **Items:** `CR-016`
- **Depends on:** T5.5
- **Blocks:** nothing
- **Surface:** configuration
- **Files:** SOP configuration for Micron EUV
**Intent:** Asset content duplicates the database Clinton's team maintains. Integration is
deferred, so the section comes out in the interim.
**Do:** Toggle Assets off for the Micron EUV SOP. Leave the section and its data model in the
application so the eventual database integration has somewhere to land.
**Done when:**
- [ ] Assets does not render for Micron EUV in the form, detail view or export
- [ ] the section and model remain in the codebase
- [ ] another SOP can enable Assets and it works
---
### T5.8 — S1 (wizard): Inline validation
- **Items:** `S1` wizard half
- **Depends on:** T5.1
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** SOP wizard validation, `wp-creation-app.js:1144` for the pattern to replace
**Problem:** Validation is `alert()`-only and `validateStep` guards only steps 1, 5 and 6,
while the markup marks required fields throughout. The message "Subject and WP Type are
required" names no field, highlights nothing and scrolls nowhere.
**Do:** Inline validation on every step that has required fields. Errors appear at the field,
the first invalid field receives focus, and the message says what is wrong. Announce via the
`aria-live` region from `T4.5`. Remove the wizard's `alert()` calls.
**Do not:** add a native `alert()`, `confirm()` or `prompt()` anywhere. The app-wide count
from wave 0 must go down.
**Done when:**
- [ ] every step with required fields validates them
- [ ] each error renders at its field and is associated via `aria-describedby`
- [ ] submitting an invalid step focuses and scrolls to the first error
- [ ] errors announce to screen readers
- [ ] the wizard's native dialog count is 0; record the new app-wide count
---
## Wave 5 exit criteria
- [ ] the stepper is keyboard operable and `<div onclick>` count dropped by at least 10
- [ ] section toggles work end to end, including the export
- [ ] Acumatica fields and Assets are hidden by configuration, with no data deleted
- [ ] the wizard has no `alert()` calls left
- [ ] first-run users can create a project

153
docs/waves/wave-6.md Normal file
View File

@@ -0,0 +1,153 @@
# Wave 6 — Work package general information
**Items:** `CR-001`, `CR-003`, `CR-004`, `CR-018`, `A7`
**Depends on:** wave 5 merged
**Why here:** these are the field's most-requested changes and they need the location lists
(`T5.4`), the server aggregates (`T4.1`) and the token system (`T3.2`) to already exist.
---
### T6.1 — CR-001: P6 Activity ID and Description
- **Items:** `CR-001`
- **Depends on:** T5.5
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** work package model, General Information section, list/dashboard, PDF export
**Intent:** Every work package traces back to the P6 schedule activity that drives it, so a
date on the work package is anchored to a schedule activity rather than floating on its own.
The meeting placed this at the top of the work package, next to the due date.
**Do:** Add `P6 Activity ID` and `P6 Activity Description` to General Information. Free text
is acceptable — a validated lookup against an imported activity list is explicitly deferred,
partly because the Micron schedule is actively being reworked.
Display the Activity ID next to the Due Date so the schedule driver is visible without
scrolling. Add it to the list/dashboard as a sortable column and to the PDF export.
**Done when:**
- [ ] both fields exist, persist, and survive a reload
- [ ] Activity ID renders next to Due Date on the detail view
- [ ] the list/dashboard column sorts correctly, including with empty values
- [ ] both fields appear on the PDF export
- [ ] the fields respect the `CR-006` section toggles
---
### T6.2 — CR-003: Priority
- **Items:** `CR-003`
- **Depends on:** T3.5
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** work package model, General Information, list/dashboard, PDF export
**Intent:** Due date alone does not convey urgency. Exactly three levels, agreed live in the
meeting: Normal, High, Urgent. Normal is the baseline default.
**Do:** A required dropdown with those three values, defaulting to Normal. Filterable and
sortable on the dashboard. Color-coded using canonical tokens only (`X7`). Printed on the
PDF export.
Priority is independent of status: it can change after issue without a status change.
**Do not:** add a fourth level. Do not introduce a new color outside the token system.
**Done when:**
- [ ] exactly three values exist; Normal is the default on a new work package
- [ ] the dashboard filters and sorts by priority
- [ ] priority colors come from canonical tokens; no raw hex added
- [ ] color is not the only signal — the label is always present
- [ ] priority prints on the PDF export
- [ ] changing priority does not alter status
---
### T6.3 — CR-004: Building / Floor / Sector dropdowns
- **Items:** `CR-004`
- **Depends on:** T5.4, T4.1
- **Blocks:** T6.4, T8.4
- **Surface:** `server/` + `html/`
- **Files:** Location section, work package model, dashboard filters
**Intent:** Users pick location from a controlled list rather than typing it. Floors within
B100 behave like separate buildings and are the unit of both execution and cost tracking.
**Do:** Three dependent dropdowns in the Location section, sourced from the project
configuration built in `T5.4`. Selecting a Building filters Floors; selecting a Floor filters
Sectors. Store codes, not display strings. Add dashboard filters for all three.
**Do not:** free-text these fields, and do not read the option lists from `localStorage`
(`X5`).
**Done when:**
- [ ] all three render as dropdowns populated from project configuration
- [ ] dependent filtering works, and clearing a parent clears its children
- [ ] values persist as codes; confirm by inspecting stored data
- [ ] the dashboard filters by each of the three
- [ ] a work package referencing a deactivated value still renders correctly
- [ ] all option data comes from the server
---
### T6.4 — CR-018: Rollup by floor and area
- **Items:** `CR-018`
- **Depends on:** T6.3, T4.1
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** aggregate endpoints, dashboard
**Intent:** This is why the Acumatica cost code was removed rather than relabeled. The
tracking dimension the team wants is floor and area, not an accounting code. For Phase 2 the
intent is to stop treating B100 as one building and monitor by floor, since each floor
carries its own instrumentation and material load.
**Do:** Extend the `T4.1` aggregates to group and total by Building, Floor and Sector. Surface
grouped totals on the dashboard. Where hours are captured (Closeout `Actual Hours`, retained
per `CR-017`), roll them up along the same dimensions.
**Done when:**
- [ ] the dashboard groups and totals by Building, Floor and Sector
- [ ] totals reconcile against an unfiltered count; verify with a known data set
- [ ] Actual Hours rolls up along the same dimensions
- [ ] grouping is computed server-side
- [ ] work packages with no location assigned appear in an explicit "unassigned" group rather than vanishing
---
### T6.5 — A7: Card status lines, footer clarity, language and time
- **Items:** `A7`
- **Depends on:** T3.5
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** card components, footer, `admin.js:484-517`
**Do:** Clarify status lines on cards and tidy the footer as proposed.
**Do not:** remove localization. `admin.js:484-517` handles language and time and it is a
shipped feature. The review specifically endorsed keeping it. If the proposal reads as
removing it, that reading is wrong.
**Done when:**
- [ ] card status lines read clearly and use the canonical status colors
- [ ] the footer is unambiguous about what it is showing
- [ ] localization still functions; verify by switching language and time format
- [ ] `admin.js:484-517` behavior is unchanged
---
## Wave 6 exit criteria
- [ ] P6 activity, priority and structured location all persist and export
- [ ] the dashboard filters by priority and by all three location levels
- [ ] rollups are server-computed and reconcile
- [ ] localization intact

297
docs/waves/wave-7.md Normal file
View File

@@ -0,0 +1,297 @@
# Wave 7 — The creator
**Items:** `B7`, `A1`, `CR-015`, `A2`, `A6`, `CR-014`, `CR-007`, `B6`, `S1` (creator), `F6`
**Depends on:** wave 6 merged, and `T4.2` (URL state) merged
**Why last among the build waves:** most of it depends on the iframe decision, and `CR-014`
depends on `X1`.
`T7.1` is the largest engineering item in the plan. Decided and approved: the iframe gets
dissolved. Do it first — everything after it is cheaper on the far side.
---
### T7.1 — B7: Dissolve the creator iframe
- **Items:** `B7`
- **Depends on:** T6.5
- **Blocks:** T7.2 through T7.9
- **Surface:** `html/`
- **Files:** parent page hosting the creator, the creator itself, cross-frame messaging
**Problem:** The tab row lives in the parent and the toolbar lives in the iframe child, so
folding one into the other is impossible while the boundary exists. The boundary is also
implicated in `F4` and possibly in `CR-006` toggle propagation (`X4`).
**Do:** Remove the iframe boundary so the creator renders in the parent document. Expect to
untangle: cross-frame messaging, duplicated styling, focus and scroll management, and
anything relying on separate document contexts.
Do this as its own PR with no feature work attached. It will be large and it needs to be
reviewable as a structural change.
**Do not:** bundle any other item into this task, and do not leave a dormant iframe path
behind a flag.
**Done when:**
- [ ] no iframe remains in the creator; grep confirms
- [ ] every creator feature works as it did before: verify against the wave 0 baseline screenshots and a functional pass
- [ ] `F4` no longer reproduces
- [ ] `CR-006` section toggles propagate into the creator with no special-casing
- [ ] browser back and forward still behave with the `T4.2` URL state
- [ ] no cross-frame messaging code remains
---
### T7.2 — F6 + A4 pattern: Section tabs replace the 4,700px form
- **Items:** `F6`
- **Depends on:** T7.1, T5.5, T3.5
- **Blocks:** T7.9
- **Surface:** `html/`
- **Files:** creator layout, jump links
**Problem:** The creator is one form roughly 4,700px tall with 11 cards rendered at once and
jump links standing in for structure.
**Do:** Give the form real structure — section tabs or an equivalent that shows one coherent
group at a time. Respect the `CR-006` toggles: a section toggled off has no tab. Retire the
jump links. Section identity belongs in the URL (`T4.2`) so a section is linkable.
**Done when:**
- [ ] no single view exceeds roughly two screen heights at 1440px
- [ ] toggled-off sections have no tab
- [ ] each section is addressable by URL and survives refresh
- [ ] jump links are gone
- [ ] tabs are keyboard operable and follow the tab pattern with `aria-selected` and arrow keys
- [ ] unsaved work is preserved when moving between sections (`T4.3`)
---
### T7.3 — A1 + CR-015: Guarded transitions and the hold bug
- **Items:** `A1`, `CR-015`
- **Depends on:** T7.1
- **Blocks:** T7.4, T7.6
- **Surface:** `server/` + `html/`
- **Files:** status handling, constraint handling, `wp-creation-app.js:1962-1972`
**This is one task, not two (`X2`).** The hold-not-clearing bug and the transition model are
the same code. `CR-015` is the highest-severity item in the plan and it was reproduced live
in front of the Micron team.
**Problem:** Clearing all constraints does not clear the hold. Hold state is not purely
derived from open constraints, which is why it goes stale.
**Do:** Model transitions explicitly. For each state, define the primary next action **plus
an audited override**. "One legal next action" is too narrow — `wp-creation-app.js:1962-1972`
contains a deliberate logged-override path for predecessors, and Issue (Hold) is a branch
from several states, not a single linear step.
Derive hold state from open constraints rather than storing it independently, or if it must
be stored, recalculate it on every constraint change. Write every hold and release to the
work package history with timestamp, user and reason.
**Do not:** remove the logged-override path. It is an audited business rule. And do not fix
the symptom by recalculating on page load only.
**Done when:**
- [ ] clearing the last open constraint clears the hold and returns the work package to its prior status, with no refresh
- [ ] logging a new constraint on an in-progress work package places it back on hold
- [ ] clearing that constraint releases it again
- [ ] the override path still works and still logs
- [ ] Issue (Hold) is reachable from every state it should branch from
- [ ] every hold and release is in the history with timestamp, user and reason
- [ ] a regression test covers the clear-last-constraint path specifically
- [ ] the exact root cause is stated in the PR
**Why the history matters:** the delay-documentation use case is the business justification
for the whole constraint workflow. If a constraint log can be shown to have been wrong, it
stops supporting a notice of delay or a change order claim.
---
### T7.4 — A2: De-duplicate the constraint warning
- **Items:** `A2`
- **Depends on:** T7.3
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** creator constraint warnings
**Problem:** The same warning appears three times.
**Do:** Keep one. The top banner is the only one visible without scrolling, so it stays. The
tab count badge then has to carry the weight the removed duplicates were carrying — make the
count prominent enough to notice from any section.
**Done when:**
- [ ] the warning appears once
- [ ] the tab badge shows the open constraint count and is legible at 390px
- [ ] with the constraints section scrolled out of view, a user can still tell constraints are open
- [ ] the banner announces via `aria-live` when the count changes
---
### T7.5 — A6: Sidebar triage data and inline hold reason
- **Items:** `A6`
- **Depends on:** T7.3
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** creator sidebar
**Do:** Surface triage data in the sidebar, including the hold reason inline. The hold reason
is already captured by the hold modal, so this is display work.
**Use case to test against:** someone in a stand-up is asked why a package has not moved.
They open it on a phone and need the answer without scrolling or clicking.
**Done when:**
- [ ] the hold reason is visible in the sidebar without opening the modal
- [ ] status, priority, due date, P6 activity and open constraint count are all visible together
- [ ] usable at 390px
- [ ] a package with no hold shows a sensible state rather than an empty slot
---
### T7.6 — CR-014: Ready for QA gate
- **Items:** `CR-014`
- **Depends on:** T7.3, T4.2 (blocking dependency `X1`)
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** status model, notification service, QA section
**Intent:** Looping QA in has historically been the failure point. Marlena raised this
directly: QA needs to see inbound work and organize their schedule ahead of time rather than
being told after the fact.
**Do:** Add `Ready for QA` between In Progress and Complete/Closed, inside the `T7.3`
transition model (`X3`). Entering that state emails the assigned QA representative and the
QA/QC members named on the work package.
The email includes the work package number and title, location, a scope summary, and **a
direct link that opens that work package** — this is why `T4.2` is a hard prerequisite. A
dashboard view lists everything sitting in Ready for QA. The QA representative can accept or
reject; rejection returns the package to In Progress with a required comment.
**Do not:** build the email with a link to the app root. That is the failure `X1` exists to
prevent.
**Done when:**
- [ ] the new state exists in the transition model, not beside it
- [ ] entering it sends email to the QA rep and named QA/QC members
- [ ] the link in the email opens that specific work package, including for a signed-out recipient via login
- [ ] a dashboard view or filter lists all packages in Ready for QA
- [ ] rejection returns to In Progress and requires a comment
- [ ] the transition is written to history
- [ ] email failure is logged and surfaced, not silent
**Note:** QA currently runs off a comprehensive Excel questionnaire being migrated into
CxAlloy. Integration with CxAlloy is explicitly out of scope. This gate is notification only.
---
### T7.7 — CR-007: Upload drawings for offline access
- **Items:** `CR-007`
- **Depends on:** T7.2, T4.3 (blocking dependency `X8`)
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** Drawings and Attachments section, file storage, export
**Intent:** Today the section holds links, typically to a Bluebeam session. The field wants
the specific PDF attached instead: a general foreman opens the package and sees exactly the
sheet and the highlighted section relevant to their scope, with no navigation and no
connectivity dependency. Links stay supported for those who want them.
**Do:** File upload alongside the existing link support. Store files with the work package.
Include them when the package is exported or downloaded. Make them available offline on a
tablet. Each attachment gets a short description field so the creator can note the focus
area, for example "Tray section, Level 3 east only".
Show file size limits and supported types in the UI before upload, not after failure.
**Do not:** build this before `T4.3`. Uploading into a form with no autosave risks losing
both the upload and the surrounding work.
**Done when:**
- [ ] PDF upload works and the file is retrievable
- [ ] links still work alongside uploads
- [ ] attachments are included in export or download
- [ ] an uploaded drawing opens on a tablet with the network disabled
- [ ] the description field persists and prints on the export
- [ ] limits and supported types are stated before upload
- [ ] upload failure is clear and recoverable, with no loss of surrounding form work
- [ ] the whole flow works at 390px
**Accepted tradeoff:** this shifts effort onto the package creator to isolate the right
sheet. That was raised in the meeting and accepted.
---
### T7.8 — B6: Sticky action bar
- **Items:** `B6`
- **Depends on:** T7.2
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** creator and wizard action bars
**Problem:** The proposal placed actions beside the fields, but on the Constraints and
Sequence steps that means scrolling to save.
**Do:** A sticky action bar instead. The creator already solves this; reuse that pattern in
the wizard rather than inventing a second one.
**Done when:**
- [ ] save and primary actions are reachable without scrolling on every step and section
- [ ] verified specifically on Constraints and Sequence
- [ ] the bar does not obscure fields at 390px
- [ ] it shows the save state from `T4.4`
---
### T7.9 — S1 (creator): Inline validation
- **Items:** `S1` creator half
- **Depends on:** T7.2
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** creator validation, `wp-creation-app.js:1144`
**Problem:** 43 native dialogs in the creator alone. `wp-creation-app.js:1144` says "Subject
and WP Type are required" without naming, highlighting or scrolling to the field, on a form
ten cards deep.
**Do:** Inline validation matching the wizard pattern from `T5.8`. Errors at the field, focus
and scroll to the first invalid one, and with section tabs now in place, the tab containing
errors is marked. Remove the creator's native dialogs.
**Done when:**
- [ ] no `alert()`, `confirm()` or `prompt()` remains in the creator; count is 0
- [ ] every required field validates inline
- [ ] submitting focuses and scrolls to the first error, switching sections if needed
- [ ] sections containing errors are marked in the tab row
- [ ] errors announce via `aria-live`
- [ ] the app-wide native dialog count is recorded against the wave 0 baseline of 79
---
## Wave 7 exit criteria
- [ ] no iframe; `F4` resolved structurally
- [ ] `CR-015` fixed with a regression test and a stated root cause
- [ ] hold state is derived, and history records every transition
- [ ] the QA gate emails a working deep link
- [ ] drawings upload and open offline on a tablet
- [ ] creator native dialog count is 0

164
docs/waves/wave-8.md Normal file
View File

@@ -0,0 +1,164 @@
# Wave 8 — Kitting and material
**Items:** `CR-009`, `CR-010`, `CR-011`, `CR-012`, `CR-013`
**Depends on:** wave 7 merged
**Decision, approved Aug 14 2026:** keep and extend Kitting, then ship it **toggled off** for
the Micron EUV SOP. Kitting is not happening at Micron today — it is complicated by
journeyman ratio and warehouse cost — but the team expects to get there, and prefab arguably
already qualifies.
That means this wave builds functionality that will not be visible on the pilot project. That
is intentional. Do not shortcut it on the grounds that nobody is using it, and do not switch
it on for Micron.
---
### T8.1 — CR-009: Kitting section retained and structured
- **Items:** `CR-009`
- **Depends on:** T7.9
- **Blocks:** T8.2, T8.3, T8.4
- **Surface:** `server/` + `html/`
- **Files:** Kitting section, SOP configuration
**Do:** Confirm the Kitting section is intact and structured to receive `T8.2` through
`T8.4`. Define kitting statuses explicitly. Proposed set, adjust only with a stated reason:
`Not Started`, `Picking`, `Staged`, `In Transit`, `Delivered`.
Then set the section off for the Micron EUV SOP via the `CR-006` toggles.
**Done when:**
- [ ] kitting statuses are defined as an explicit set, not free text
- [ ] the section is off for Micron EUV and absent from its form, detail view and export
- [ ] a different SOP can enable it and it works fully
- [ ] no kitting data is deleted anywhere
---
### T8.2 — CR-010: Warehouse Owner dropdown
- **Items:** `CR-010`
- **Depends on:** T8.1
- **Blocks:** T8.3
- **Surface:** `server/` + `html/`
- **Files:** Kitting section, dashboard filters
**Intent:** A named person owns fulfillment of the kit. Today that is Paul Coonrod and
everything bottlenecks through him informally, so the work package should record it
explicitly.
**Do:** A Warehouse Owner dropdown populated from project members. That person receives
kitting notifications by default. Filterable on the dashboard so a warehouse owner can see
everything assigned to them.
**Done when:**
- [ ] the dropdown lists project members and persists a selection
- [ ] the selected owner is a default recipient for `T8.3` notifications
- [ ] the dashboard filters by warehouse owner
- [ ] removing someone from the project does not break existing work packages referencing them
---
### T8.3 — CR-011: Kitting status change notification
- **Items:** `CR-011`
- **Depends on:** T8.2, T4.2 (blocking dependency `X1`)
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** notification service, kitting status handling
**Intent:** Extends the existing status-change notification pattern to kitting so the field
knows when material moves from requested to staged to delivered without chasing it in Teams.
**Do:** A kitting status change emails the work package distribution list. The email states
the work package, the old and new status, who changed it, the delivery location, and a direct
link to the work package. Match the existing status-change email pattern so users see one
convention, not two.
**Do not:** send a link to the app root (`X1`).
**Done when:**
- [ ] every kitting status change sends to the distribution list
- [ ] the email includes old status, new status, actor, delivery location and a working deep link
- [ ] the link opens that specific work package, including via login for a signed-out recipient
- [ ] formatting and tone match the existing status-change email
- [ ] rapid consecutive changes do not produce a burst of near-identical emails
- [ ] send failures are logged and surfaced
---
### T8.4 — CR-012: Delivery location on Kitting / Material
- **Items:** `CR-012`
- **Depends on:** T8.1, T6.3
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** Kitting section, print/pick output
**Intent:** Delivery logistics are the real pain. Staging at the warehouse, getting it on a
truck, then making sure material reaches the correct floor lay-down, shark cage or conduit
tree rather than being picked at will by whoever is closest.
**Do:** Capture delivery location using the same Building / Floor / Sector lists as `CR-004`,
plus a free-text detail field for specifics like lay-down area, shark cage or conduit tree.
Include the delivery location on the kitting notification email and on any pick or delivery
printout.
**Done when:**
- [ ] delivery location uses the shared location lists, not a parallel free-text field
- [ ] the detail field persists and prints
- [ ] the location appears on the `T8.3` email
- [ ] it appears on any pick or delivery printout
---
### T8.5 — CR-013: Lightweight material request
- **Items:** `CR-013`
- **Depends on:** T8.4, T7.2 (sequencing dependency `X6`)
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** Materials section, request model, dashboard
**Decision, approved Aug 14 2026:** build the lightweight version. Defer the parts catalog,
live inventory and warehouse integration.
**Intent:** Material requests run today through per-floor Teams chats and a spreadsheet, all
funneling to Paul Coonrod. The comparison drawn in the meeting was a project where requests
went into a OneNote and became, in the words used at the time, word vomit. The point of this
item is structure, not features.
**In scope:** line items (description, quantity, unit), needed-by date, requestor, delivery
location (reuse `T8.4`), and status.
**Out of scope, do not build:** master parts catalog dropdown, live inventory levels,
warehouse system integration.
**Do:** Build the in-scope set. Requests are visible on the dashboard and filterable by
delivery location and status.
**Do not:** build this before `T7.2`. A material card added to the pre-tab creator is a 12th
card on a 4,700px form. And do not invent a parts list — Nate's spreadsheet and the master
material workbook have not been supplied yet (see `IMPLEMENTATION.md` section 8).
**Done when:**
- [ ] line items can be added, edited and removed, with quantity and unit
- [ ] needed-by date, requestor, delivery location and status all persist
- [ ] requests appear on the dashboard, filterable by location and status
- [ ] no parts catalog, inventory count or warehouse integration exists in the diff
- [ ] the section respects `CR-006` toggles
- [ ] the whole flow works at 390px, since requests originate in the field
---
## Wave 8 exit criteria
- [ ] Kitting is fully built and toggled off for Micron EUV
- [ ] notifications carry working deep links and a delivery location
- [ ] the material request is the lightweight scope, nothing more
- [ ] no guessed part numbers or floor names anywhere in the code

217
docs/waves/wave-9.md Normal file
View File

@@ -0,0 +1,217 @@
# Wave 9 — Verification and cleanup
**Items:** `CR-008`, `CR-017` (guard), `S6`, `S7`, `S8`, `C1` audit, `C2` audit, part of `C4`
**Depends on:** wave 8 merged
**Why last:** `CR-008` reviews the PDF export against the final structure rather than
reviewing it twice. The audits confirm the standing requirements actually held across eight
waves rather than being asserted per PR.
---
### T9.1 — CR-008: PDF export review
- **Items:** `CR-008`
- **Depends on:** T8.5
- **Blocks:** nothing
- **Surface:** `server/` + `html/`
- **Files:** PDF export
**Problem:** The export was never walked through in the meeting. It needs a pass to confirm
it prints what the field actually needs and nothing it does not, now that the work package
structure has changed substantially.
**Do:** Review the export section by section against the final structure. Sections toggled
off under `CR-006` must be omitted. The export must include: P6 Activity ID and Description,
Priority, Building / Floor / Sector, scope of work, materials, constraints with status,
QA/QC, and an attachment index. Removed fields — ACU Cost Code, Acumatica Task — must not
appear.
Open question to raise in the PR, not to decide alone: should attachments merge into a single
PDF package, or be listed as separate files? The field preference expressed in the meeting
was to hand someone exactly the sheet they need and nothing more, which argues for a merged
package with only the attached sheets. Propose, do not assume.
**Done when:**
- [ ] every listed field appears in the export
- [ ] toggled-off sections are absent
- [ ] ACU Cost Code and Acumatica Task appear nowhere
- [ ] the export is legible printed in black and white
- [ ] the export is legible opened on a tablet
- [ ] the attachment index lists every attachment with its description
- [ ] the merge-versus-list question is raised in the PR with a recommendation
---
### T9.2 — CR-017: Actual Hours guard
- **Items:** `CR-017`
- **Depends on:** T9.1
- **Blocks:** nothing
- **Surface:** verification only
- **Files:** Closeout section
**This is a guard, not a build.** Removing Actual Hours was floated in the meeting and
rejected — Marlena said she likes tracking actual hours spent so it can be measured. It is
listed here so a later simplification pass does not quietly drop it.
**Do:** Confirm Actual Hours still exists in Closeout, is optional rather than required, and
survived eight waves of change.
**Done when:**
- [ ] Actual Hours exists in Closeout and persists
- [ ] it is optional, and a work package can be closed without it
- [ ] it rolls up per `T6.4`
- [ ] a follow-up item is logged for comparing actual against estimated hours to produce a productivity factor
---
### T9.3 — S6: One icon system
- **Items:** `S6`
- **Depends on:** T9.1
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** every page
**Problem:** The icon set mixes emoji and dingbats, and at least one glyph is used for two
different meanings on the same page.
**Do:** Pick one icon system and convert everything to it. Build a mapping of meaning to icon
and record it in `docs/reference/tokens.md`. Resolve every duplicate meaning.
**Do not:** leave emoji in the UI. They render differently per platform, which is part of why
the same glyph reads as two things.
**Done when:**
- [ ] one icon system across all 6 pages
- [ ] no glyph carries two meanings anywhere
- [ ] every icon has a text label or an accessible name
- [ ] the meaning-to-icon mapping is documented
- [ ] icons render identically on Windows, macOS and a tablet
---
### T9.4 — S7: Sample-data cleanup
- **Items:** `S7`
- **Depends on:** T9.1
- **Blocks:** nothing
- **Surface:** `html/` + `server/`
- **Files:** every sample-data affordance
**Problem:** Four sample-data affordances under three names. One sits a single click from
live project data with no confirm and no undo.
**Do:** Reduce to one affordance with one name. Move it away from live project actions. Add a
confirmation stating exactly what will happen, and make it reversible or clearly scoped to a
sample project only.
**Done when:**
- [ ] exactly one sample-data affordance exists, under one name
- [ ] it is not adjacent to any live project action
- [ ] it confirms before acting, naming what it will do
- [ ] it cannot touch live project data; verify by attempting it against a real project
- [ ] the other three affordances are removed; grep confirms
---
### T9.5 — C1 + S8: Accessibility audit
- **Items:** `C1`, `S8`
- **Depends on:** T9.3
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** all pages, `.help-tip` elements
**`S8` is finished here** because tooltips are the one accessibility item with no natural
host component earlier in the plan. 15 `.help-tip` badges are `<span>` elements with no
`tabindex`, so the `:focus` half of their CSS rule is dead code and there is no touch path at
all — on tablets, which is exactly what Field View targets.
**Do:** Make every `.help-tip` reachable by keyboard and by touch. Then audit the whole app
against the `CLAUDE.md` rules and record results in `docs/reference/accessibility-audit.md`.
Compare against the wave 0 baseline counts:
| Metric | Baseline | Target |
|---|---|---|
| `<div>` / `<span>` with `onclick` | 12 / 2 | 0 |
| `.help-tip` unreachable by keyboard | 15 | 0 |
| `aria-live` regions | 0 | at least 1 per toast system and banner |
| Text below 4.5:1 | present | 0 |
| `outline: none` without replacement | present | 0 |
| Native dialogs (`alert`/`confirm`/`prompt`) | 79 | 0, or documented exceptions |
**Done when:**
- [ ] every `.help-tip` works by keyboard and by touch
- [ ] every metric above meets target, or the gap is documented with a reason and a follow-up item
- [ ] a full keyboard-only pass completes the primary flow: sign in, create project, SOP wizard, create work package, issue it
- [ ] the audit document exists with per-page results
---
### T9.6 — C2: Mobile pass
- **Items:** `C2`
- **Depends on:** T9.5
- **Blocks:** nothing
- **Surface:** `html/`
- **Files:** all pages
**Problem:** Nothing in the original proposal touched mobile, and it is where the worst
rendering was found. Field View at 390px is the gloved-hands surface.
**Do:** A deliberate pass at 390px across all 6 pages and the full primary flow. Capture
after screenshots and compare against the wave 0 baseline.
**Done when:**
- [ ] the full primary flow completes at 390px without horizontal scrolling
- [ ] tap targets are at least 44px throughout
- [ ] no control is clipped, overlapped or unreachable on any page
- [ ] `CR-007` attachments open at 390px, offline
- [ ] `T8.5` material requests can be raised at 390px
- [ ] after screenshots for all 6 pages at 390px are committed alongside the baseline
---
### T9.7 — Final reconciliation
- **Items:** all
- **Depends on:** T9.6
- **Blocks:** nothing
- **Surface:** documentation
- **Files:** creates `docs/reference/completion.md`
**Do:** Walk all 55 items from `IMPLEMENTATION.md` section 6 and record, for each: built /
deferred / not applicable, the PR that delivered it, and any deviation from its written
acceptance criteria.
Explicitly confirm the four out-of-scope items in section 7 were not built: no parts catalog
or inventory, no asset database integration, no CxAlloy integration, no P6 activity import.
Then list what the plan learned that the next revision should carry: items whose acceptance
criteria turned out wrong, inputs still outstanding, and follow-ups logged along the way.
**Done when:**
- [ ] all 55 items are accounted for with a PR reference or a stated reason
- [ ] every deviation from written acceptance criteria is documented
- [ ] the four out-of-scope items are confirmed unbuilt
- [ ] outstanding inputs from `IMPLEMENTATION.md` section 8 are restated with current status
- [ ] follow-ups are collected in one place for the next revision
---
## Wave 9 exit criteria
- [ ] the export matches the final structure
- [ ] one icon system, one sample-data affordance
- [ ] accessibility metrics hit target or are documented
- [ ] the primary flow works at 390px
- [ ] every item is reconciled