Commit Graph

19 Commits

Author SHA1 Message Date
e3de3c7c00 T7.8 - B6: the wizard's actions ride the viewport, not the page
On the Constraints and Sequence steps the proposal's beside-the-fields actions
meant scrolling to save. The wizard's .step-navigation bar is now
position:sticky at the viewport bottom - the creator's sticky-bar pattern,
adapted rather than duplicated: sticky (not the creator's fixed) because the
bar lives inside the wizard's grid column, keeps its slot in the flow, and
therefore CANNOT obscure a field at any width - no padding arithmetic to get
wrong. Opaque background, top border and the shared --wp-shadow-sticky token
so content scrolling beneath it reads as beneath it.

The T4.4/B5 save-state indicator already mounted in this bar; it now rides the
viewport with the buttons, which is the "shows the save state" criterion.

Verification (each probe run alone): NEW tests/sticky_bar_check.py 12/12 - a
primary action inside the viewport on all 12 steps unscrolled at a 700px
viewport (short on purpose: both named steps genuinely overflow, asserted),
still visible fully scrolled, nothing obscured at 390px. Regression:
stepper_check ALL PASS.

Items: B6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:14:33 -07:00
12d19446d5 T7.1 - B7: dissolve the creator iframe, and D1 give it back its sample data
There is no iframe in html/ any more. The creator is a top-level document with
the same app bar and the same tab strip as the SOP wizard; the two tabs that
used to swap a frame are links between them.

DEVIATION, stated rather than smuggled. The wave file says "remove the iframe
boundary so the creator renders in the parent document". It renders as its own
document instead. Every done-when is met - no iframe, no cross-frame messaging,
F4 resolved structurally, CR-006 toggles with no special-casing, back and
forward intact with T4.2's URL state - but the route is the other one, and the
reason is in creator-frame.md's own numbers:

                                    merge into parent    make it a page
  selector collisions to resolve                   21                 0
  script global collisions                          9                 0
  cross-frame call sites to remove                 28                28
  probe entry points needing rework               ~29                 2

The 21 and the 9 were never the cost of dissolving the boundary. They are the
cost of MERGING TWO DOCUMENTS, which is a different change the boundary was
hiding. And 29 probe call sites address wp-creation-index.html directly, so a
route that keeps that address keeps all of them. creator-frame.md section 5
records this in full.

What went, and what replaced it:

  #wp-frame, applyEmbedLayout, sizeWPFrame, viewportMinusChrome, chromeHeight,
  renderWPTab, the resize handler, the ResizeObserver, --wp-chrome-h,
  .content-area.embed-full, body.embed-full   ->  the window sizes the page

  ?embedded=1, body.embedded, .embed-hide, .embed-first   ->  nothing. An old
  link carrying the param is ignored rather than half-obeyed.

  openWpById / showDashboard / showForm / dashApplyFlag / applySopSections
  called across the frame   ->  the URL. ?project= ?view= ?wp= ?flag= were
  already read at the creator's own boot (T4.2), which is exactly why those four
  could be DELETED rather than migrated. X4 is closed: the surviving path is the
  one T5.5 built and proved.

  inIframe in auth-guard.js, wp-chrome.js, wp-sidenav.js, help.js and _isTop in
  project-data.js   ->  gone. help.js now reads the explicit WP_HELP_NO_FAB flag
  both tool pages set, instead of inferring intent from where it is rendered.

  .main-nav / .nav-tab in work-package-suite-styles.css   ->  wp-chrome.css,
  because a tab row only one of two documents can style is the shape that put
  the tabs in the parent and the toolbar in the child to begin with.

The three questions creator-frame.md section 4 said no count could answer:

  1. The creator gets the app bar. It was the only page loading neither
     wp-chrome file. Its header is now the .header-left / .header-right pair the
     wizard uses, so the switcher lands in the same place on both.
  2. Two sequence components, scoped not merged - confirmed Aug 18 that the
     sequence is authored in the SOP and adjustable per package. BL-015 stays.
  3. body.embedded is gone. The header it hid is replaced by the app bar; the
     sample controls are visible in a new package toolbar (D1); the analytics
     button is visible there until T7.10 moves it. The Dashboard BUTTON in that
     row became a TAB, which is the one place B7's "fold the toolbar into the
     tab row" actually happened.

Old addresses still resolve. ?tab=wp, ?view=dashboard and ?wp=<id> are in
bookmarks, in wp-sidenav's link map, and they are the shape CR-011 and CR-014
were specified against (X1). The wizard forwards them with replace(), so Back
does not bounce. Breaking these silently was the one regression this task could
have shipped that nobody would notice for weeks. frame_check.py section 4 pins
all three.

BEHAVIOUR CHANGE, deliberate. The live cross-frame hand-off showed the creator a
section toggle that had NOT been saved: flip it, look, reload, and the section
came back. What the creator shows now is the SOP that is stored. sections_check
5b pins both halves - an unsaved toggle does not travel, a saved one does.

BEHAVIOUR CHANGE, not deliberate, logged as BL-020. A tab switch is a page exit
now, so leaving the wizard with unsaved SOP edits fires T4.3's unsaved-work
guard. Nothing is lost - the guard writes the draft first and T4.3 recovers it -
but it is friction that did not exist, and suppressing a deliberate guard is a
product decision with its own downside. Logged, not quietly handled here.

tests/frame_check.py, 39 checks, new. Two of them exist because of failures
during this task rather than in it:

  - "both documents parse and boot". A const shadowing a function parameter is a
    SyntaxError, and work-package-suite-app.js did not parse at all for one run.
    Four checks in url_state_check went red and not one said "the script did not
    load". Asserting a page's own entry points exist costs nothing.
  - "focus emulation is on, so a focus reading means something". An earlier draft
    called page.call instead of page.ws.call inside a try/except and measured
    nothing, reporting no focus ring anywhere - which looks exactly like a
    finding. Trap 5 in reverse, for the second time in this project.

The four backlog entries logged against this file, re-measured rather than
assumed:

  BL-001  still reproduces (485px in a 390px viewport) but its RECORDED CAUSE IS
          WRONG. --nav-w now computes to 56px, so the injected-style explanation
          is spent. The overflow is the creator's data tables - #asset-body's
          lays out at 520px with no scroll container. frame_check reports the
          offending boxes by selector and skips position:fixed subtrees, because
          the comments drawer parked off-screen at right:844 made the first
          measurement blame the drawer. Pinned, not fixed: T7.2 lays out the form.
  BL-013  CLOSED. It was fixed by S12 in WAVE 4 - wp-creation-styles.css:209
          carries the comment naming this entry - and nobody updated it. It was
          quoted as a live CLAUDE.md violation while planning wave 7 and had not
          been true for four waves. a11y_check walks 120 focusable elements on
          the creator and every one rings at >= 3:1.
  BL-006  15 by the probe's measure, unchanged; different denominator, stated.
  BL-007  68 raw radii by the probe's measure. Nothing has reduced it in four
          waves; it is measured every run now instead of once.
  BL-018  cost a FOURTH probe. frame_check imports set_sop from sections_check
          rather than writing a fifth copy of the workaround. T9.9 owns it.

Probes re-pointed, with reasons in the files: sections_check 5b (drove the live
hand-off), pipeline_check check 2 (read through contentDocument), f_items F4
(drove standalone and embedded; there is one mode now), validation_check
(lost "the wrong tab", gained the SOP gate).

Verified: frame_check 39/39, sections_check 95/95, pipeline_check 44/44,
url_state_check 23/23, validation_check 83/83, a11y_check 22/22,
autosave_check 34/34, aggregates_check 16/16, stepper_check 71/71,
browser_check 71/71, launcher_check 58/58, generalinfo_check 49/49,
rollup_check 63/63, cards_check 44/44, locations_check 58/58.
f_items: F1-F5 fixed, F6 reproduces (T7.2).
Metrics: iframes 1 -> 0, colour literals in rules outside theme-light.css 0,
dialogs 64, <div onclick> 2, .help-tip 18.

Items: B7 D1
Task: T7.1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:26:27 -05:00
55caefb099 T5.8 - S1 (wizard): errors at the field, and the last dialog is gone
S1 has two halves and they are easy to conflate.

One is that validation was a native dialog: "Please complete all required
fields: Project Name, Number, Client, Division, and Site Location." names five
fields at once, highlights none of them, and scrolls nowhere. The other is that
validateStep guarded steps 1, 5 and 6 while the MARKUP marks required fields on
1, 3, 5, 6 and 7 - so two steps' asterisks meant nothing at all, which is worse
than no asterisk.

Both fixed, and the second is the one worth measuring: the probe reads the
required-field list out of work-package-suite.html rather than out of STEP_GATES,
because a probe that read the table would agree with whatever the table says and
prove nothing. Both notations count - an asterisk in a <label>, and the red span
beside step 3's role titles.

Now: an error per FIELD, rendered at it, associated by aria-describedby, marked
aria-invalid, announced through role="alert", and the first one focused and
scrolled into view. The error boxes are BUILT from the gate table rather than
written into the markup twelve times - adding a required field is one row, and
its error element, its association and its announcement all follow. A
markup-side error box somebody forgets to add is an error nobody ever sees.

An error clears as you type rather than on the next submit. An error still
showing over a field you have just corrected teaches people to ignore errors.

And nothing paints a step you have not tried to leave: the rail asks
stepGateMet(), which reads the same fields and marks none of them.

The thirteen dialogs

  Every one is now the thing it should have been - an error at the field it is
  about, or an announcement in a live region with the role T4.5 established:
  errors interrupt, confirmations do not.

  A dialog is not merely ugly. It blocks the page, cannot be placed or styled, a
  screen reader can present it only as a modal interruption, and it is one OK
  button whatever it says - so "sample data loaded" and "you cannot do that"
  arrived identically.

  Two deserve naming. The empty-comment alert became an inline error on the
  feedback textarea. And showAnalytics() was a confirm() carrying the entire
  usage summary as its body - a wall of text in a dialog whose only dismissal
  was also the download button. The summary is the useful part, so it is shown,
  with the download offered as an action beside it. That function has no caller
  in the wizard's markup (the "Usage data" button is the creator's, calling the
  creator's own showAnalytics), and it was converted rather than deleted:
  deleting a feature is not what this task was asked to do, and its dialog
  counted toward the number this task has to drive to zero.

  html/work-package-suite.html       #wp-toast, an error box on the textarea
  html/work-package-suite-app.js     STEP_GATES widened; per-field messages;
                                     ensureErrorBoxes; wizardToast; 13 removals
  html/work-package-suite-styles.css .wp-toast
  tests/validation_check.py          new - 81 checks
  tests/stepper_check.py             its "the alert T5.8 still owns" check now
                                     asserts the opposite, by name

Done when
  [x] every step with required fields validates them - 5 steps, from the markup
  [x] each error renders at its field and is associated via aria-describedby
  [x] submitting an invalid step focuses AND scrolls to the first error
      (scroll checked by bounding box, not by trusting scrollIntoView)
  [x] errors announce to screen readers
  [x] the wizard's native dialog count is 0

The count, recorded both ways because BL-017 says the metric counts prose

  work-package-suite-app.js   0 raw, 0 with comments stripped
  app-wide                   64 raw, 64 stripped, against wave 0's 79
                             wp-creation-app.js 43 (wave 7), users.js 10 and
                             admin.js 6 and index.html 5 (wave 9)

The wizard contributes none of what is left, which the probe asserts rather
than leaving to the total.

Verified one at a time
  validation_check 81/81  new
  stepper_check    71/71
  sections_check   88/88
  browser_check    71/71
  a11y             22/22
  url_state        23/23
  autosave         34/34
  locations        58/58
  aggregates       16/16
  pipeline         43/43
  launcher         58/58
  f_items          F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added. The toast says "error" by a red rule AND by staying
until dismissed where a confirmation times out - two channels, not one (C1).

Question for the PR, per CLAUDE.md: step 3's two role TITLES are validated
because the markup marks them required, but the two role NAME pickers beside
them are not marked and so are not gated. A sign-off role with nobody in it is
arguably the more useful thing to catch. The markup is what was built to; if the
intent was the names, that is two rows in STEP_GATES.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 12:30:32 -05:00
c453e50412 T5.6 - CR-002: Acumatica cost code and task, hidden by toggle
The team concluded these two are noise on a field work package: cost codes are
effectively constant on a job 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.

Hidden, not removed. CLAUDE.md: "Removed fields are hidden, not deleted (CR-002,
CR-016). Retain the data and the model." So this is a second, narrower toggle
list beside T5.5's sections - two fields inside General Information rather than
two more sections, because a section is a block of the document and these are
two rows in one.

  no migration           the values live in the work package's JSON data blob,
                         which nothing here writes to. The probe greps every
                         migration for a drop_column touching either.
  no model change        server/models.py is untouched by this task
  no code change to      the toggles are SOP data. Another project turns them
  re-enable              back on from step 12 and both fields return, values
                         included

A field is on only if its own toggle is on AND the section holding it is. Asked
as one question (WPSections.fieldOn) so no caller has to remember to ask both -
a field showing inside a hidden section is not a state worth reasoning about,
and the probe checks that case explicitly.

  html/wp-sections.js            FIELDS, fieldOn, normalizeFields
  html/work-package-suite-app.js field rows nested under their section
  html/work-package-suite-styles.css .field-toggle
  html/wp-creation-index.html    ids on the two .field wrappers
  html/wp-creation-app.js        WP_FIELD_NODES; both document rows conditional
  tests/sections_check.py        +22 checks (53 -> 75)

Done when
  [x] neither field appears in the form, detail view or PDF export when off
  [x] existing records still hold their values - a package EDITED while both are
      off comes back through collectPackage() with both intact
  [x] the fields can be re-enabled for another SOP without a code change
  [x] no schema migration drops data - checked against every migration in the
      tree, not just the ones this wave added

The whole .field wrapper is hidden, not the input: a bare label over nothing is
worse than either state.

Raised, not fixed
  BL-019  A cost code that has left COST_CODES is silently blanked on edit.
          wp_cost is a <select>, and setting .value to something with no matching
          <option> does nothing at all - so opening such a package clears the
          field and the next save writes the blank back. The same bug was fixed
          once already for gov_wosize (work-package-suite-app.js:490-495) by
          adding the stored value as an option; cost code never got it.

          Found the honest way: a probe here used an invented cost code to prove
          hiding a field does not delete its value, and the value came back
          empty. That looked exactly like the toggle eating data. It was not, and
          the probe now uses a real code and says why in a comment - a probe that
          fails for a reason other than the one it names is worse than no probe.

Verified one at a time
  sections_check  75/75  (53 + 22 for CR-002)
  browser_check   71/71
  stepper_check   70/70
  a11y            22/22
  url_state       23/23
  autosave        34/34
  locations_check 58/58

Question for the PR, per CLAUDE.md: BL-000b asks whether General Information
wants per-field toggles generally. This is not that - it is the two fields
CR-002 names, and the list is deliberately closed. If a third field wants one,
that is the general question and it needs the product answer BL-000b is holding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 12:10:28 -05:00
ae30c58337 T5.5 - CR-006: section toggles, and X4 resolved rather than deferred
X4 first, because the brief asks for it explicitly.

IMPLEMENTATION.md sequences CR-006 after B7 on the grounds that the toggles must
suppress sections inside the creator, which is an iframe child until T7.1. The
wave file puts it in wave 5 anyway, and its last done-when is written to
accommodate exactly that: "toggle state propagates into the creator, OR the PR
documents exactly where it does not and why".

It propagates. Both ways, by two separate paths, because they fail differently:

  ON THE CREATOR'S OWN BOOT the flags ride on the SOP, which the creator already
  reads - ProjectData.pullProject hydrates it from the server. Nothing crosses
  the frame boundary at all, so this path is unaffected by B7 either way. It
  covers a reload, a fresh tab, the standalone creator page, and a colleague
  opening the project on another machine.

  WHILE THE FRAME IS OPEN the wizard hands the change straight across
  (pushSectionsToCreator -> cw.applySopSections), the same shape T5.3 used for
  the dashboard filter. Without it, flipping a toggle would appear to do nothing
  until a reload.

T7.1 removes the second path, not the first. That is the whole of the X4
exposure and it is one function, commented as such. Building CR-006 after B7
would not have made the SOP-borne path any different; it would only have saved
writing the hand-off.

What it does

  Ten sections, one shared list (html/wp-sections.js) read by the wizard, the
  creator's form and the creator's rendered document. Three surfaces meant three
  chances to drift, which is how "Assets is off, except in the export" happens.

  Off means NOT RENDERED - form, detail view and PDF export. It never means
  deleted. renderPackage() was rebuilt from one long string into a list of
  (section, html) blocks so a suppressed section leaves no empty heading and the
  survivors renumber 1.0, 2.0, 3.0 instead of leaving a hole. The print window
  reuses that same HTML, so the export needed no separate change.

  Absent means ON. A SOP saved before today mentions no sections, and reading
  that as "all off" would blank every project in the estate the moment this
  shipped. WPSections.normalize is the one place that decision lives.

  html/wp-sections.js               new - the shared list, defaults, normalize
  html/work-package-suite.html      step 12, a 12th rail button
  html/work-package-suite-app.js    the toggles, state.sections, the hand-off
  html/work-package-suite-styles.css the toggle rows
  html/wp-creation-index.html       stable ids on the five unnamed cards
  html/wp-creation-app.js           WP_SECTION_NODES, applySopSections,
                                    renderPackage rebuilt as blocks
  tests/sections_check.py           new - 53 checks

Done when
  [x] all 10 sections are toggleable - each one driven off and back on
  [x] a section toggled off is absent from the form, the detail view and the
      PDF export - checked by content marker, not by heading, so "the section
      is gone" and "the section was empty" cannot be confused
  [x] toggling off then on restores prior data with no loss - and the sharper
      version: a package EDITED while Assets is off still carries its assets
      through collectPackage(), which is what Save uses
  [x] new SOPs default to all sections on
  [x] toggle state propagates into the creator - both paths, separately

Two things worth arguing with

  General Information is toggleable, because CR-006 enumerates it. Turning it
  off leaves nothing to identify a package by. The row says so in its own note
  rather than being quietly excluded from the list.

  Location has no card of its own - it is a field inside General Information's
  grid, and its toggle governs that one row. CR-004 gives it structured
  building/floor/sector fields in wave 6, at which point one line of
  WP_SECTION_NODES changes. Written down because "the toggle does nothing" and
  "the toggle governs one row" look identical from outside.

  Approvals & Sign-offs is NOT toggleable and is not one of the ten. A package
  nobody signed is not a shorter package.

Verified one at a time
  sections_check  53/53  new
  stepper_check   70/70  (STEP_COUNT 11 -> 12)
  locations_check 58/58  (its "step 11 is last" check now asserts the thing that
                          is actually invariant - the wizard's navigation follows)
  browser_check   71/71
  a11y            22/22
  url_state       23/23
  autosave        34/34
  aggregates      16/16
  pipeline        43/43
  launcher        58/58
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added: still 0 across all page sheets and inline blocks. Each
toggle says its state three ways - the checkbox, the words "In use" / "Not used",
and the rule down its left edge (C1).

Question for the PR, per CLAUDE.md: the toggles are SOP-wide, so a project
cannot use Kitting on install packages and not on BIM ones. BL-000b already
records the field-level version of this question; the per-WP-type version is the
same shape and is not recorded anywhere yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 11:25:05 -05:00
2081c1ad3c T5.4 - CR-005: a per-project location taxonomy, stored as codes
CLAUDE.md lists CR-005 among the change requests that get "silently half-built if
you treat them as frontend-only". This is the server half and the wizard half
together: a new table, four routes, an Alembic revision, and step 11.

CODES, NOT DISPLAY STRINGS, because CR-018 rolls cost up by these values and a
rollup keyed on a label breaks the day somebody fixes a typo in it. Two columns
carry that: `code` is a node's own slug, derived once at import and never
recomputed; `path` is the full slug path, unique per project, and is what a work
package will store. Renaming a value changes `name` alone - the probe renames a
floor and demands its path comes back byte-identical, with its children's paths
intact.

DEACTIVATE, NEVER DELETE. There is no DELETE route, and the probe checks for its
absence (405) rather than trusting that nobody added one. Deactivating hides a
value from new work packages and cascades DOWN, because a floor nobody can pick
must not keep offering its sectors. Reactivating walks UP only - a sector may
have been switched off for its own reasons, and silently resurrecting it would
undo a decision nobody made twice. That asymmetry is deliberate and is pinned by
a named check so it does not get "fixed" into a surprise.

Import reports rather than merges. Rejected rows come back with the SOURCE line
number and a reason; duplicates are listed as duplicates, separated into "already
in this project" and "already on line N of this import". Reusing a parent is not
a duplicate - B1/L2/1P and B1/L2/2P share a building and a floor by design, and
only the full path repeating counts. Re-importing a deactivated value brings the
same row back rather than creating a second one; the probe checks the id.

One parser, on the server. A CSV is read in the browser and posted as text
exactly as a paste is, so "what does a blank column mean" has one answer.
Comma, semicolon and tab all work - a paste out of a spreadsheet is tab
separated and a saved CSV is not, and which one somebody has is a question the
machine can answer.

No guessed floor names. IMPLEMENTATION.md section 8 says the B100 list has not
been supplied. The seeded sample has "Sample" inside every string, and the probe
greps html/ and server/ for a location-shaped assignment containing any of the
review's real names.

  server/models.py                    LocationNode
  server/alembic/versions/e2a4c7d91b30_location_taxonomy.py
  server/app.py                       GET/POST/PATCH + import, parser, slug
  html/work-package-suite.html        step 11, an 11th rail button
  html/work-package-suite-app.js      the step's logic; LAST_STEP replaces 10
  html/work-package-suite-styles.css  the list, the report
  html/theme-light.css                .field-error, now declared once
  tests/locations_check.py            new - 58 checks
  tests/stepper_check.py              STEP_COUNT 10 -> 11

Done when
  [x] CSV upload and paste both work and report rejected rows with reasons
  [x] duplicates are detected and reported rather than silently merged
  [x] values are editable after import - rename, add, deactivate
  [x] deactivating hides it from new work packages; an existing package
      referencing it still resolves, because the row is retained
  [x] values are stored as codes suitable for grouping
  [x] no guessed real-world floor names exist anywhere in the code

Two decisions worth disagreeing with

  Step 11, appended, not step 2, inserted. Locations belong beside Project by
  subject. Renumbering 2-10 would touch every sop-step-N id, every
  collectStepData case, every gate key and the analytics history - a large
  silent-mismatch surface for an ordering change. The count now lives in one
  place (LAST_STEP), so reordering later is cheap.

  Any project member may edit the list, not only a Project Admin. It matches how
  the SOP baseline itself is authored: the Project Admin gate is on CHANGING a
  completed SOP, not on writing one. If the location list should be tighter than
  the SOP it belongs to, that is a product call.

Verified one at a time
  locations_check  58/58  new
  stepper_check    70/70  (11 steps)
  browser_check    71/71
  a11y             22/22  sop now rings 38 focusable elements
  url_state        23/23
  autosave         34/34
  aggregates       16/16
  pipeline         43/43
  launcher         58/58
  f_items          F1-F5 FIXED, F6 REPRODUCES (T7.2)
  alembic          upgrade / downgrade / upgrade all clean on a throwaway SQLite
                   file, and the migrated schema matches Base.metadata.create_all
                   column for column - dev auto-creates and production migrates,
                   so a divergence between the two is invisible until it ships

.field-error was declared in two page sheets by the end of T5.2 and would have
been three by T5.8, so it moved to theme-light.css. No colour literal added
anywhere: still 0 across all page sheets and inline blocks.

Question for the PR, per CLAUDE.md: the levels are fixed at building / floor /
sector. Micron's floors behave like buildings, which this handles by letting a
project use whichever levels it needs - but a job that wants a fourth level, or
different names for the three, cannot say so. Whether that is worth a
per-project level vocabulary is a product question; the schema would take it
without a migration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 11:06:27 -05:00
5e1f6e75ba T5.1 - A4/S9: a vertical stepper, ten real buttons, states in words
The step rail was ten div elements carrying onclick inside a horizontal
scroller. Not in the tab order, not operable by keyboard, and silent about
progress - the only thing on the page that said where you were was a "1 / 10"
pill in the app bar, detached from the control it described.

The rail is now a vertical column beside the form: ten <button> elements in an
<ol> inside a named <nav>, with arrow keys, Home and End on top of the Enter and
Space a button gives for free. All ten stay in the tab order; a roving tabindex
would have satisfied "arrow keys" by breaking "tab", which the done-when asks
for both of.

Four states, each carrying a word and a marker shape as well as a colour (C1):
Complete (green disc, tick), Current step (blue disc, aria-current="step"),
Locked (dashed outline) and a plain default. Locked steps keep aria-disabled
rather than disabled, so a keyboard user can reach one and be told what is in
the way instead of finding a control that has vanished from the tab order.

Reachability is the guard's own rule, deliberately not a stricter one: you may
leave the step you are on once its required fields are filled. The tempting rule
- lock everything after the first unmet gate anywhere - is not what
validateStep() enforces, and a padlock the Next button walks straight past is
the drift this change exists to remove. validateStep() and the rail now read one
STEP_GATES table, so they cannot disagree; T5.8 widens that table rather than
editing four functions.

Clicking a step you cannot reach announces why through a role="alert" region and
puts the cursor in the field that is missing. Saying "no" and leaving you where
you were, with no idea which of five inputs was empty, is what the dialog did.

Below 900px the rail collapses to a disclosure naming the step you are on -
ten vertical rows above the fields is most of a 390px screen before you reach an
input. 44px tap targets, since Field View is the gloved-hands surface.

Also: going backwards is no longer gated. previousStep() never validated, so a
rail that did would have trapped you on an incomplete step.

  html/work-package-suite.html        rail markup, counter removed
  html/work-package-suite-styles.css  #tool-sop grid, .step-rail*, 899px collapse
  html/work-package-suite-app.js      STEP_GATES, renderStepRail, keyboard, watcher
  tests/stepper_check.py              new - 70 checks

Done when
  [x] all 10 steps are <button> elements
  [x] keyboard: tab, arrow keys, Home/End, Enter and Space
  [x] aria-current on the current step, exactly one
  [x] complete / current / unavailable told apart without colour
  [x] the "1 / 10" counter is gone - no .step-counter, no N/10 in the app bar
  [x] app-wide <div onclick> 12 -> 2, down exactly 10

Verified one at a time
  stepper_check   70/70   new
  browser_check   71/71
  url_state       23/23
  a11y            22/22   sop now rings 37 focusable elements, all >= 3:1
  autosave        34/34
  aggregates      16/16
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)
  baseline_shots  14 shots; only the sop pair changed. The beforeunload log on
                  sop@1440 and creator@1440 is present at HEAD too - captured
                  both sides to check rather than assume.

No colour literal was added: all five page sheets and all seven inline <style>
blocks still hold zero. New spacing consumes --wp-s*; three raw font sizes were
added and three removed, so BL-010 is unchanged in kind.

Raised, not fixed
  BL-016  Back to a URL with no `step` leaves the wizard where it was. T4.2's
          popstate handler parses NaN and ignores it; its own probe never took
          that branch. stepper_check pins the current behaviour by name so the
          fix has a test waiting.
  BL-017  The native-dialog baseline counts the word `alert(` in comments. Four
          comments written here - all of them about removing a dialog - moved
          the number from 80 to 82 while two real calls were being deleted. They
          were reworded; the metric still needs a comment-stripped variant, which
          T5.8 owns.

Question for the PR, per CLAUDE.md: BL-015 leaves the creator's .step-tab
uppercase as the last forced-uppercase interactive text in the suite, on the
grounds that A5 scopes sentence case to buttons and field labels. The wizard's
rail is now buttons, so its labels are sentence case ("Sign-offs", "WP types").
The two are consistent by rule and inconsistent on screen until T7.x.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 23:19:58 -05:00
c024cba844 T4.5/T4.6/T4.7 - S10/S11/S12: it announces, it is legible, focus is visible
Three small accessibility items, done together because they share one probe and
one measurement method. tests/a11y_check.py, 22 checks, all passing.

S10 — ANNOUNCEMENTS. The app had zero aria-live regions; login.html's
role="alert" / role="status" pair was the only correct example in the codebase.
Both toasts now take an optional kind and set the role BEFORE the text, because
assistive technology announces on the content change and a role applied after
describes the next message rather than this one. The sync badge announces
politely.

Admin banners are handled by a rule rather than by editing thirteen assignment
sites: a MutationObserver in console-util.js marks anything carrying `.bad` as
role=alert and everything else role=status. Thirteen edits is thirteen chances to
get it wrong, and any banner added later would have missed out. The probe checks
a banner created after load, which is the case that would have regressed.

S11 — CONTRAST. Re-measured rather than quoting either published figure, as the
file map asked. #8d8d8d is 3.32:1 on white, not "about 2.9:1" as the plan says;
console.css:103's 3.3:1 was right. On the shaded surfaces it is worse - 3.01:1 on
a success banner. --cds-text-helper (#6f6f6f) clears 4.5:1 on all seven
backgrounds the app actually paints, tightest 4.56:1.

Fixed once, in the token, so all three sheets inherit it. console.css's local
override is gone, as the task requires. Disabled text was repointed to
--cds-text-disabled rather than darkened with everything else: making disabled
text MORE legible makes a disabled control look enabled.

The probe measures against the background actually painted behind each element,
walking ancestors for the first non-transparent one - not an assumed white, which
is how "passes on paper" and "fails on the page" come to disagree.

S12 — FOCUS. An app-wide :focus-visible floor in theme-light.css at zero
specificity, so any component can still draw its own. Filled controls get an
explicit rule at class specificity: a blue ring inside a blue button measures
1.00:1, which is not a subtle problem but no indicator at all. console.css's
inset ring had exactly that defect on button.primary.

`outline: none` is down from six to one, and that one (.wpc-search-input) has its
replacement in the rule above it - the shell rings on :focus-within, and ringing
both would draw two rectangles.

TWO REAL DEFECTS THE PROBE FOUND that reading would not have:

  - .wp-navbtn's ring is white, which is right on the near-black app bar and
    invisible on the creator's white header. Same button, same class, two hosts,
    1.00:1 on one of them.
  - The comment drawer is translated off-screen when closed, and a transform
    moves a thing without removing it from the tab order. Its name field,
    textarea, Add button and close button were all still focusable: a keyboard
    user could tab into a panel they could not see and could not tell they were
    in. Now visibility:hidden while closed, with the transition delayed so it
    still animates both ways.

The probe itself needed three corrections, each of which was a wrong answer
before it was a right one, and each worth knowing:

  - focus emulation must be ON, or :focus-visible never matches in headless and
    every element reports clean - a pass that means nothing.
  - which surface a ring is drawn against depends on the offset the BROWSER uses,
    not the one the stylesheet asked for. Chromium redraws a low-contrast author
    ring in white at offset 0 on a filled control, which is more contrast than was
    requested; measuring that against the parent scores it 1.00:1 and calls a
    correct ring a defect.
  - focus() on a hidden control does nothing, so the probe has to ask whether the
    focus actually landed. A closed drawer still has layout; a bounding box is not
    evidence that anyone can reach it.

Metric 7, aria-live regions: was 0 at wave 0, now 13 role/aria-live sites across
7 files.

browser_check 71/71, f_items 5 FIXED / F6 REPRODUCES.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 22:20:57 -05:00
ce6fb840f0 T3.5 - A5: one button system, and green goes back to being a status
251 buttons across the 7 pages, counted in the browser with every wizard step,
creator section and tool panel forced visible. Two thirds of them are
display:none at load, so a static grep sees about eighty and misses the rest.

FOUR ROLES, defined once in theme-light.css as --wp-btn-*, and no fifth:

  primary    the one action the screen exists for. Filled accent.
  secondary  every other real action. White, --border-strong, accent on hover.
  tertiary   navigating or undoing. No fill, no border, accent text.
  danger     destructive. Outlined red; filled red only where the control is too
             small for an outline to read - the 28px x on a sequence row.

Every button class is mapped to a role in docs/reference/tokens.md section 12.
No value is new: these are the fills the sheets already rendered, given one
definition so that "primary" means one thing.

GREEN IS A STATUS COLOUR AND NO LONGER FILLS A BUTTON. A5 names two green action
buttons; there are four. .use-btn and the launcher's completed-SOP card button
never render green in the default fixture, so the review could not have seen
them - the SOP has to be finished and a suggested value has to be offered first.

  .nav-btn.primary            "SOP complete"    wizard
  .btn.btn-generate           "Save & view"     creator
  .use-btn                                      creator
  .card.complete .card-button                   launcher

The green did not go anywhere. .cstatus button.on-cleared, .toggle-btn.enabled,
.wp-nav-dot.ok, .rb-ready, .badge-R and the launcher card's own left border and
status line all still carry it, and every one of those is a state rather than an
action. The launcher card in particular still says "complete" twice after this
change; it just no longer says it on the button.

SENTENCE CASE, applied to buttons and field labels only, which is the scope A5
sets. First word capitalised, the rest lowercased, acronyms and external proper
nouns left alone (SOP, QC, WP, UPN, PM/APM/CM/QM, PDF, JSON, CSV, BIM, MIMO,
Excel, Acumatica).

  ~30 button labels across launcher, wizard, creator, admin and two scripts
  46 field labels
  text-transform:uppercase removed from 4 rules - .btn and .add-btn (creator
  buttons), label and .cmt-namebar label (creator field labels)

Labels carrying markup - a .req asterisk, a .help-tip chip - had only their text
nodes transformed, so the markup survives and "first word" means the first word
of the label rather than of each fragment. The creator's mono face, 10px size and
tracking are its idiom and are untouched; only the forced uppercase goes.

help.js was updated too. It names "Load Sample" and "SOP Complete" in prose, so
renaming the buttons without it would have left the help centre describing
controls that no longer exist. That coupling is the only place in the app where
button text is referenced by name.

Verified by re-running the inventory: 0 green action buttons, 0 uppercase button
labels, 251 buttons still present - nothing was lost in the rename.

console.css card headers are unchanged, confirmed by diff: the only six lines
this task touches in that file are token substitutions on button/button.primary/
button.danger, none of them within twenty lines of .card h2.

f_items 5 FIXED / F6 REPRODUCES. browser_check 71/71.

Left alone and logged: .step-tab is still uppercase (BL-015) - it is a stepper
tab, neither a button nor a field label, and A4/S9 rebuild the stepper. Table
headers, section eyebrows and headings keep their case throughout. BL-008 and
BL-009 were re-targeted from T3.5 to wave 9: both are colour merges on a field
fill and a status pill, and this task is scoped to buttons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:11:56 -05:00
03d24138ed T3.4 - A3/F5 full, BL-002: the wizard's fields look editable and focus is visible
T1.5 stopped editable fields looking disabled by reaching past the wizard's own
token block with a fallback: background: var(--cds-field, var(--bg-card)). T3.2
deleted the local tokens, so the fallback is now dead code and the canonical
token is simply consumed. The interim comment is gone; grep for INTERIM in the
sheet returns nothing, and the sheet declares no field-specific colour.

Measured in the browser rather than asserted - every enabled text control in the
suite, side by side:

  sop .field input        #ffffff on #8d8d8d 1px
  sop .user-pick          #ffffff on #8d8d8d 1px
  creator .field input    #ffffff on #8d8d8d 1px
  console .toolbar input  #ffffff on #8d8d8d 1px
  launcher .proj-row      #ffffff on #8d8d8d 1px

.user-pick was not identical before this commit, and that is the one thing here
beyond the written task. The sign-off name pickers were --bg on a --border
hairline - #f4f4f4 on #e0e0e0, which is precisely the grey-on-grey that F5/A3 is
about. They were missed because the review counted inputs inside .field and these
two sit outside it, which is why they needed their own rule in the first place.
Fixing the token duplication without fixing them would have left the defect on
the page while reporting it closed.

Disabled fields stay distinguishable: f_items F5 reads them at #f4f4f4 on the
same #8d8d8d border, so fill carries locked-vs-editable and the border does not
move.

BL-002 folded in, as that entry asks. All three outline:none sites in this sheet
are gone:

  .field input/select/textarea:focus  had a 3px --primary-light glow
  .user-pick:focus                    had the same glow
  .seq-step input.seq-label:focus     had NO replacement at all

#edf5ff against a #ffffff field is a 1.05:1 edge - a faint halo on the card and
nothing at all on the field. Replaced with the ring console.css:69 and
wp-chrome.css:206 already draw, so this is the app's existing idiom rather than a
fourth one: 2px of --cds-focus, inset by -2px over the control's own edge.

Verified with CDP focus emulation ON. Without it the headless page is not the
focused document, :focus never matches, and every reading comes back as the
unfocused style - which looks like a pass and is not one. It cost a wrong answer
before I noticed, so it is worth saying. .user-pick and .seq-label also live on
later wizard steps, which are display:none and cannot take focus, so the probe
reveals the steps before measuring:

  .field input / select / textarea    2px solid #0f62fe  offset -2px
  .user-pick                          2px solid #0f62fe  offset -2px
  .seq-label                          2px solid #0f62fe  offset -2px

f_items 5 FIXED / F6 REPRODUCES. browser_check 71/71.

Two findings outside this task's files, logged rather than fixed: BL-013, the
creator's inputs have NO outline on focus and rely on the same 1.05:1 glow, which
is BL-002's defect in the sheet next door and belongs to T7.2; and BL-014, four
controls on the launcher and field view fall back to the UA default ring, which
is visible but is a fourth idiom - T9.5 with C1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:00:59 -05:00
2485ca7b61 T3.3 - S5: fix .field-hint properly and retire the comment it left behind
The comment at work-package-suite-styles.css:395 recorded the consequence of the
four token systems: .field-hint's only rule lived in wp-creation-styles.css,
which the wizard does not link, so the class rendered as unstyled body text. The
rule was later added to the wizard sheet, but as a SECOND rule with declarations
byte-identical to the .field small rule four lines above it - the same
duplication that caused the original bug, in miniature.

Merged into one rule under both selectors, so they cannot drift apart again, and
deleted the comment. It described a fixed problem, and its account of where the
class is used was wrong anyway: .field-hint appears once on the wizard, not in
"step 2's CM hint, the team-member notices".

The colour is deliberately untouched. --text-dim is #8d8d8d, which measures
3.3:1 on white and which console.css:103 already rejected for exactly this job -
but propagating that fix is T4.6, which is told to re-measure rather than inherit
either published figure. Fixing it here would change a rendered value in a wave
that must produce none, and would take the decision away from T4.6.

THE HUNT for the same class of failure elsewhere, which is the rest of the task.

Statically, per page, over every stylesheet that page actually links plus its own
inline <style>: every bare var(--x) reference where --x is declared by no sheet
the page loads. That is the .field-hint failure exactly, and it is the one that
renders nothing rather than rendering wrong.

  login 32 refs, launcher 54, sop 67, creator 92, admin 100, users 100, field 56
  unresolvable: 0 of 501

Zero, and structurally zero from here on: after T3.2 every token resolves from
theme-light.css, which all seven pages link first. The failure mode is no longer
possible rather than merely absent.

In the browser, on all seven pages: every class on a real element that no rule in
any loaded stylesheet targets, including the sheets help.js and wp-creation-app.js
inject at runtime. One candidate, and it is not a defect - .team-pick on the
wizard's four project-team selects is a JS hook that sits beside data-team, and
the selects are inside .field, so .field select styles them. Its sibling
.user-pick needs its own rule only because those two selects sit OUTSIDE .field,
which the comment there already explains.

Stated as a limitation rather than left implicit: the browser half sees each page
in its load state, so a class applied only on interaction would not appear in it.
The static half has no such gap - it covers every rule in every sheet regardless
of what is on screen - and it is the half that catches this particular bug.

Verification: token_check reports every token and all 3,500 elements identical to
wave 2 on all 7 pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 18:54:17 -05:00
9ab7b48de2 T3.2 - C3/S5: one source of truth for colour; page sheets alias only
theme-light.css is now the only file in html/ that contains a colour literal.
The five page stylesheets and all four inline <style> blocks declare names and
nothing else.

  theme-light.css                 191 declarations, 175 with a literal value
  console.css                      28 declarations,   0
  work-package-suite-styles.css    16 declarations,   0
  wp-chrome.css                    14 declarations,   0
  wp-creation-styles.css           24 declarations,   0
  wp-sidenav.css                    0 declarations,   0

#0f62fe is declared in one sheet, down from five. The eleven occurrences left
inside theme-light.css are Carbon's own v10-to-v11 alias layer, which the
inventory records as deliberate and not the S5 defect.

Names were kept, because 111 var() references live in .js files across 23 token
names and a rename there fails silently - no build error, no console warning,
just an unstyled element.

The rule the refactor was built on: consolidation is not unification. Where two
sheets declared the same value, they collapse. Where they declared DIFFERENT
values for one role - the two shadows, the eight status borders doing four jobs,
the three mono stacks - each value got its own canonical name and the pair is
recorded for T3.5. Picking a winner between two near-identical greys is a
rendered change, which this task forbids. The console's zebra stripe is the one
that would have bitten: #fafafa is six points from #f4f4f4, and merging them
erases the striping on the nine-column user table.

Collecting the one-offs in one place made two things countable that were not
before: twelve distinct shadows, and a ninth amber (#8a6d00 on the field view,
four points from #8e6a00 and doing the same job - BL-009).

VERIFICATION - the screenshot done-when could not do the job, so it was replaced.

Captured against wave 2, 11 of 14 shots were pixel-identical and 3 were not.
Capturing wave 2 against ITSELF produced the same 3 differences at the same
bounding box, so those shots cannot distinguish a regression from the clock.
Trap 2 in the brief is half wrong: users.html is stable at both widths; the
unstable third is the creator at 1440px, and admin's captured page height varies
by ~600px between runs (BL-012).

So tests/token_check.py was added. It checks what wave 3 actually claims: that
every custom property resolves to the same literal, and every element computes
the same colours, shadows and type. That is stronger than a screenshot - it
covers the hover, focus and disabled rules a screenshot never exercises, and it
is deterministic.

  wave 2 vs T3.2, all 7 pages:
  178/178 wave-2 token names resolve identically, +213 new
  3,500 elements compute identically, zero added, zero removed
  16 tokens differ in notation only (#fff -> #ffffff), which is the duplicate
  class this task existed to collapse

Two detours worth not repeating: the element walk was first keyed by sibling
index and reported 55 phantom differences on the SOP page, where three
JS-injected overlays append in whichever order their async work finishes
(BL-011); and the comparator now normalises notation before reporting, because
otherwise it fails on its own success.

f_items 5 FIXED / F6 REPRODUCES as expected. browser_check 71/71.

ONE DONE-WHEN NOT MET, recorded rather than skipped: "no page stylesheet
declares a raw color, spacing or type value". The colour half is met in full.
483 raw spacing values, 281 font-sizes and 65 radii remain inside rules, 492 of
them in the creator. That is arithmetic, not effort: the creator's spacing is
every integer from 1px to 14px, so no token exists that padding:9px 11px maps to
without changing one of the numbers - and this task forbids changing a rendered
value. The two requirements are mutually exclusive. Logged as BL-010 for T5.x
and T7.1, where those pages are re-laid-out and the values get chosen again.

New backlog: BL-009 (ninth amber), BL-010 (raw spacing/type in rules),
BL-011 (overlay append race), BL-012 (unstable screenshot targets).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 18:50:30 -05:00
5f3141e2a3 T1.5 - F5 (interim): wizard fields stop looking disabled
INTERIM. T3.4 removes the duplicate token underneath this; the job here is only
the appearance, and no token consolidation is started.

The wizard filled its inputs with var(--bg) - which in this sheet is the PAGE
BACKGROUND, #f4f4f4 - on a #e0e0e0 border. An empty required field was
indistinguishable from a locked one, which is why people were not typing in
them. The cause is the one the review named: this sheet redeclares its own
tokens, so it never saw --cds-field: #ffffff, even though theme-light.css has
been supplying that to this page all along.

Fields now consume --cds-field, and take the same --border-strong the creator's
inputs already use, so a field looks like a field on both pages. No new value is
introduced - both tokens already existed.

That inverts a signal if left there, so it needed the other half: there was no
disabled rule at all on this page, meaning locked fields would have turned white
too. Disabled and readonly fields now take --cds-field-02, the theme's own
secondary field surface, matching .locked-field in the creator. Enabled #ffffff
against disabled #f4f4f4, verified by computed style rather than by eye.

The border is deliberately the same on both states. I first wrote
`border-color: var(--border)` on the disabled rule and could not demonstrate it
taking effect - the rule matches, is more specific than the base rule, and its
background applies, but the computed border stayed --border-strong. Rather than
ship a declaration whose effect I cannot show, it is gone: a consistent border
is what "consistent with inputs elsewhere" asks for, and the fill is what
carries the state.

Screenshot diff is limited to the wizard, but establishing that took a control
run. admin and users appeared to change too, until capturing twice with NO code
change showed they differ from themselves - the console pages render live
timestamps and are not byte-stable. login, launcher, sop, creator and field are.
Recorded in the baseline README so the next task with a "no layout change"
done-when does not chase it.

The F5 probe now also fails if enabled and disabled fields become identical,
which is the way this fix could silently go wrong.

f_items: F1-F5 FIXED, F6 untouched as wave 1 requires. browser_check 71/71.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 18:48:47 -05:00
d9f96f20e1 T1.3 - F3: give the SOP header's groups a defined relationship
.header-left and the injected chrome were in a dead tie. Both were
`flex: 1 1 auto` with `min-width: 0`, so both claimed the same run of the bar
and both were allowed to shrink to nothing. The chrome's content is wider, so
it won every time: .header-left computed to clientWidth 0 while its
flex-shrink:0 logo kept its 106px and overflowed underneath the project
switcher. With the real project name that meant "2667008" rendered on top of
the PRIME wordmark and the name itself clipped to "on EUV Cleanroom En...".

The bar now has an order of giving way rather than a tie:

  .header-left    flex: 0 1 auto, min-width: auto   sizes to content, floors at
                                                     the logo plus the gap
  .wp-chrome      flex: 1 1 auto (unchanged)         the only one that grows
  .header-right   flex: 0 0 auto                     keeps its buttons

min-width:auto restores the content-based floor the explicit `min-width: 0` had
removed. The inner title block keeps its own min-width:0, so the project name
still gives way first, through the ellipsis .header-subtitle already carries -
truncation policy stays B2's, and nothing here silently truncates.

Two things the review did not name were colliding on the same bar and are fixed
with it. .header-right was being squeezed below its buttons, so "Load Sample"
ran underneath "Feedback". And the header was a fixed 48px holding FOUR groups,
not two - the markup's two plus what wp-chrome.js and auth-guard.js inject - so
at 1024px the overflow had nowhere to go but on top of its neighbours, and
T1.2's user-menu wrap turned that into three rows spilling onto the tab row
below. min-height plus flex-wrap lets the bar grow instead.

Header height at 1440px with a normal project name is still exactly 48px, so
desk layout is unchanged; sop-1440 differs from the wave 0 baseline only
because T1.1 gave the switcher a name to show in place of "(unnamed)". With the
long name it grows to 62px at 1440 and 82px at 1024 - wrapping rather than
overlapping, which is the point.

The F3 probe was too narrow to have caught the right-hand collisions: it
compared the logo against the chrome and nothing else. It now checks every pair
of groups sharing the bar, plus anything spilling out of it, and still reports
FIXED at 390, 768, 1024 and 1440 with the long name.

Verified at all four widths with "Micron EUV Cleanroom Enable 2667008": no
overlapping pair, nothing spilling, logo fully visible.

browser_check 71/71. f_items: F1, F2, F3 FIXED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 18:33:15 -05:00
e3527a6e1d Act on the fragility audit: boot-order crash, real cache correctness, deep links
A 55-agent audit of the last few commits confirmed 32 findings. The high and medium
ones are fixed here; the ranked leftovers are listed at the end.

Boot-order crash (my regression, wave 2)
- wp-format.js loaded AFTER wp-creation-app.js on every page, but the creator boots
  synchronously at parse time and its comment renderer calls wpFormatDateTime(). With
  any review comment present that threw a ReferenceError and aborted the rest of boot.
  The formatter now parses before the app scripts on all five pages. Verified with a
  comment seeded: the date renders and boot completes.

The network-first fix didn't actually work
- `fetch(req)` inherits the request's default cache mode, so it consults the browser
  HTTP cache — the previous commit's "network-first" still allowed a page to run
  against a stale sibling. Code is now fetched with cache:'no-cache' and precached
  with cache:'reload'.
- Nothing pinned freshness on the wire either: no Cache-Control anywhere, so browsers
  applied heuristic caching (~10% of a file's age) and each file expired at a
  different moment. NGINX and the dev server now send no-cache for html/css/js/
  webmanifest; images stay cacheable. Verified on the wire.
- Non-ok responses were returned verbatim, so a 502 broke pages the cache could have
  served; they now fall back to the cache. Cache keys drop the query string, which
  fixes both the offline miss on every in-app link (?project=…&tab=…) and unbounded
  cache growth. respondWith can no longer resolve to undefined. Cache bumped to v5.

Embedded creator
- Dropped the &t=Date.now() cache-buster and made the frame's identity the PROJECT.
  The view and which package to open are now applied by calling into the loaded
  document, so switching tabs no longer reloads it — that reload discarded unsaved form
  edits, made the creator unreachable offline, and stored a fresh copy per click.
- ?view=dashboard was re-read on every tab switch, so after one deep link the
  "Work Package Creation" tab kept opening the Dashboard for the rest of the session.
  Deep-link params are consumed once now.
- ?wp=<id> — which the global search has been emitting since wave 2 — was read by
  nothing, so picking a work package in search opened a blank one. The creator now
  exposes openWpById() and the shell applies it after a new 'wp-creator-ready' event,
  because the frame's load fires before pullProject() resolves.
- Math.max(320,…) could make the frame taller than the space available while page
  scrolling was disabled, pushing content off a window that couldn't scroll. Full-bleed
  is now only used when at least 460px remains, and the SOP-incomplete gate never runs
  inside it. A ResizeObserver re-measures when wp-chrome.js grows the app bar.

Contract drift
- .field-hint and .user-pick are used on the SOP suite page but their only rules lived
  in wp-creation-styles.css, which that page doesn't link — the CM hint and the
  sign-off pickers had no styling at all. Rules added to the suite's stylesheet.
- The creator's critical floor now also hides modal overlays (a stale stylesheet
  rendered their contents inline in the form) and gives the jump bar a sane sticky top.
- login.js dereferenced ids unguarded where the old version guarded, so a cached older
  login.html would break sign-in itself. Guarded.
- The "Language & time" menu item was added only if wp-format.js had already parsed;
  the check now happens at click time.

Verified: 157 API checks across five suites on a clean database, plus 22 driven UI
checks — boot-with-comment, tab switching with a no-reload probe, short-viewport
fallback, and the search deep link landing on the right package.

Not done, ranked: ~50 dead CSS rules across three stylesheets; dead .team-pick and
.constraint-option contracts; wp-chrome.js's documented '.header' mount branch is
unreachable because the creator loads neither wp-chrome.js nor its CSS; the squeeze
half of the embed layout (.content-area.embed-full) is still CSS-only, which degrades
to the old narrow column rather than breaking; fingerprinted asset URLs would make a
mismatched pair unrepresentable rather than merely unlikely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:07:20 -07:00
fcba74b584 Fix the WP navigator and the squeezed embedded layout; add per-project permissions
Layout — the reported "skinny scrolling windows"
- .content-area capped the whole suite at 1000px, so on a 1920 screen the embedded
  Work Package Creator ran in a ~930px column with its own scrollbar inside the
  page's. The wizard now caps at 1700px and the Creator/Dashboard tab goes
  full-bleed: the iframe fills the window below the app chrome and owns the only
  scrollbar. Needed `flex: none` on the content area — as a `flex: 1` item its
  flex-basis overrode `height`, leaving the used height indefinite so the child's
  `height: 100%` collapsed the iframe to its 150px default.
- The SOP wizard's fields were one per row; they now flow into ~340px columns.

Navigator — now an auto-hiding drawer
- It was a fixed 262px column that stole width from the form AND was hidden below
  1100px, so embedded (the normal path) it never appeared at all — that's the
  "broken side menu". It's now an overlay drawer behind a slim always-visible edge
  handle: hover or tap to open, move away / Escape / pick a package to close, or pin
  it to keep it open (pinned shifts the form and the page chrome across, and is
  remembered). A gutter keeps the handle off the section-nav chips.

Bugs found while checking the site over
- collectStepData() still read the SOP team fields as text inputs, but wave 1 made
  them account pickers — so it wrote a user ID into state.team.pm where the display
  NAME belongs, and the SOP would print `user_ab12…` as the PM. Now synced properly
  from the pickers.
- loadSampleData() set .value on those selects with fictional names; setting an
  unmatched value on a <select> silently does nothing, so the sample lost its team.
  It now stores them as names without an account, which the picker shows as
  "(no account)".
- My earlier CSS block replacement had deleted the SOP-chip, people-picker and
  critical-tag styles. Restored.

Same picker everywhere the SOP names someone
- Sign-off roles (step 3, required and optional) are account pickers now, storing
  userId alongside the name, so a signature belongs to an account that can be
  notified. Titles stay free text.

Per-project permissions (asked for: "change project permissions for individual users")
- project_members.role overrides the account's role on that project, so a PM on one
  job can be a Project User on another. Empty = inherit; app admin is admin
  everywhere. effective_role() feeds require_project_admin, so WP delete, completed-
  SOP edits and project delete are all judged per project.
- Project access is now its own column in the admin console (it was buried among the
  action buttons, which is why it couldn't be found), showing the project count per
  account; the dialog sets access plus the role on each project.
- The members endpoint reports each person's effective role on that project.

Verified: 157 API checks across five suites on clean databases (44 permissions +
22 password reset + 34 search/localization + 39 gates/notifications + 18 new
per-project permission checks), 16 drawer-behaviour + 4 pinned-mode UI checks driven
in headless Chrome, and probes confirming the team/sign-off pickers populate and no
longer corrupt state.team on step navigation. Screenshots reviewed at 1920x1080.

Service-worker cache bumped to v3 so browsers pick up the new shell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 17:22:19 -07:00
61d1cf4bff Wave 2: form cleanups from the site comments, plus localization, project switcher and global search
Site comments (8/3)
- BIM card: LOD removed, IFF # added next to the coordination status, and required
  once that status is "Signed off (IFF)" — an unnumbered sign-off isn't traceable.
  A LOD already stored on a package is preserved and shown as legacy, not blanked.
- The blue "from SOP types" subtext under a field is now a SOP chip on the label
  with the detail in a tooltip. The chip stays visible rather than hover-only:
  field tablets have no hover, and "this came from the SOP" is the part that
  matters. The hint elements stay in the DOM (hidden) so the code writing to them
  keeps working; an observer mirrors their text into the tooltip.
- Specification Section is no longer typed per package. Each WP type carries a
  spec section on the SOP; the field is read-only in the Creator and follows the
  type, with the SOP's spec folder linked underneath. This reads both spec
  comments as one intent — stop typing it, derive it.
- Assignees and Distribution are multi-selects over the SOP project team, showing
  each person's job function, with the CM pre-added to Distribution (removable per
  package) and a free-text option for people with no account. The stored display
  strings are unchanged so print/export/dashboard keep working; account ids ride
  alongside for the notification work in wave 3.

Localization + time
- Per-user locale/timezone (Language & time in the user menu), an app-wide default
  in the admin console, then the browser. Timezones are validated against the
  server's zoneinfo and the picker is fed from it. Calendar dates are formatted
  from their parts so a due date never reads a day early in another zone.
- Every displayed timestamp now goes through the shared helpers.

Top-bar chrome
- Project switcher beside the logo and a centered global search, injected into
  either generation of top bar; skipped in an iframe so the embedded Creator
  doesn't get a second one. Ctrl/Cmd-K focuses search.
- GET /api/search covers work packages, projects and SOPs, scoped to the caller's
  projects, hiding archived packages, with LIKE wildcards escaped.

Fixed along the way: showForm() cleared every card's inline display, which undid
applyKind() — so the Package Type and BIM cards reappeared on an install-only
project. Split out applyKindVisibility() and re-apply it there.

Verified: 100 API checks on a fresh database (44 permissions + 22 password reset +
34 search/localization), 24 driven UI checks against the real Creator page in
headless Chrome (SOP chips, both people pickers, spec auto-fill, critical tags,
BIM suppression), and the chrome harness on both bar styles. Screenshots reviewed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:17:51 -07:00
39b48055ff Productionize WP Suite: auth, security hardening, sync, dashboard, PWA, email
Brings the Work Package Suite from a browser-local prototype to a
multi-tenant, SQL-backed deployment hardened for customer IP.

Auth & access control
- Local username/password login (bcrypt + JWT in an HttpOnly cookie),
  admin-managed users, per-project membership, and project-scoped API access.
- Admin console: change user roles, view the audit trail, manage settings.

Security hardening
- CSP / HSTS / X-Frame-Options / nosniff headers in nginx; Secure cookie via
  X-Forwarded-Proto; CSRF Origin check; attribute-safe output escaping.
- Login lockout, token_version session revocation, stronger password policy,
  fail-closed secret loading, encrypted (AES-256) database backups.

Persistence & schema
- SOPs and Work Packages are now DB-backed and shared across users, written
  through a durable client sync outbox that queues offline edits.
- Alembic migrations applied automatically on container start.

New capabilities
- Phase 2 dashboard (progress, gating, pagination, archive).
- Phase 3 PWA "Field View" with offline caching and auth fallback.
- WP owner assignment with OPTIONAL email notifications, OFF by default and
  toggled from the admin console. SMTP password is read only from the
  SMTP_PASSWORD env var (never stored); emails carry a WP number + deep link,
  never customer IP.

Also: IBM Carbon restyle, Help section, and DEPLOYMENT.md brought up to date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:51:15 -07:00
fd668f0ea2 Move static files into html/ for Docker bind mount 2026-06-15 14:47:24 -05:00