Commit Graph

27 Commits

Author SHA1 Message Date
2a5f6b3549 C4 fix - five undefined token names rendered surfaces transparent
The T9.9 token sweep pointed seven files (help.js, auth-guard.js, wp-format.js,
project-data.js, index.html, field.html, wp-creation-app.js) at Carbon names
the theme never defined: --cds-layer-01/-02, --cds-border-subtle-01/-strong-01,
--cds-layer-hover-01. theme-light.css carries no -01 suffixes. An undefined
var() invalidates the whole declaration, so the help-centre modal, the
change-password and language dialogs, the print popup's inlined values, the
creator nav drawer and the sync badge all rendered TRANSPARENT backgrounds -
reported by Nick against the help menu, 2026-08-20.

Renamed every consumer to the canonical tokens (--cds-layer, --cds-layer-accent,
--cds-layer-hover, --cds-border-subtle, --cds-border-strong), matched to the
hex each replacement originally stood in for. color_check gains check 3: every
var() consumed anywhere must resolve to a definition somewhere - the class of
this bug, pinned. Verified live: the modal computes rgb(255,255,255) over an
opaque gray nav, and the language dialog is opaque too. BL-025 logged for the
one wrong-base-colour rgba tint noticed in passing.

Item: C4 (regression in its own enforcement). Probe: color_check 5/5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 11:27:12 -07:00
771672273d T9.8 - D7: archiving stops reading as deletion - for project admins
Archiving already froze a project (the server refuses every write); what did
not exist was the way back in. Now:

- GET /api/projects?archived=only|all filters the answer BY PER-PROJECT ROLE:
  a project admin (or super/app admin) on THAT project sees it; everyone else
  receives an empty list from the same request - archived projects appear
  nowhere for them, counts and pickers included (the default listing already
  excluded them for everyone; asking is what got gated). Admin-on-Job-A does
  not surface archived Job B.
- The launcher gains a visibly separate, labelled "Archived projects"
  section (dashed border, read-only stated in words), rendered only when the
  server returns rows. Opening one makes it active; the launcher's reconcile
  learned that an active project whose stored summary says archived:true was
  opened ON PURPOSE and keeps it, while a project archived out from under
  someone still drops with the existing explanation.
- The creator shows ARCHIVED - READ-ONLY where the project is named (both
  ctx-bar branches, from the SERVER's answer - the page's project comes from
  the URL, so a stale local summary is not trusted) and refuses saves with a
  reason before the round trip. The courtesy; the server's refusal is the
  rule, verified by calling the endpoints directly (wp upsert AND the
  material-list write both refuse with "archived" even for an admin).
- No unarchive button, no second mechanism, and it fits at 390px.

Verification (each probe run alone): NEW tests/archived_check.py 15/15.
Regressions: launcher_check 58/58, sample_check 10/10, export_check 20/20,
frame_check 38/38.

Items: D7

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:58:51 -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
815f266039 T6.5 - A7: cards say their state, the footer says what it is showing
A7's "do not" is louder than its "do", so that first: admin.js:484-517 handles
language and time, it is a shipped feature, the review specifically endorsed
keeping it, and if the proposal reads as removing it that reading is wrong. It
is UNTOUCHED. The probe checks that by diffing the file against HEAD as well as
by driving the feature — switching locale, saving, reloading, and confirming the
saved value came back from the server.

Card status lines

  Two things were doing one job badly. The SOP card said "SOP complete" when it
  was and NOTHING when it was not — so the commonest state on a live project was
  the one with no status line at all. And the Work Package card carried its state
  in its BUTTON ("Complete SOP first", "Checking..."), which is a button
  describing a situation instead of naming what pressing it does.

  Now every card says its state in its own line, in all three states, and no
  button changes text to report one:

    complete    green, the canonical success token
    not yet     secondary text - a real answer, and neither green nor a warning
    unknown     the suite's amber, and it names the failure

  Each carries a glyph and a word as well as a colour. The line is replaced in
  place rather than removed and re-added, because a card that briefly has no
  status line reads as "no status" and that is one of the three real answers.
  role="status" on it: the text is written by a fetch that lands after the page
  has settled, which is what aria-live is for (S10).

Footer

  Was "Work Package Suite v1.0 | Prime Controls - Business Technology Group |
  Pilot Use Only", which leaves three questions open: v1.0 of what, who Business
  Technology Group is to this page, and what Pilot Use Only actually restricts.

  Now two sentences. The first names the product and who maintains it. The
  second says what "pilot" means in the only terms that matter to somebody about
  to type a real work package into it: the work is real and is kept, the tools
  around it are still changing. The bare version string is gone rather than left
  claiming to be a version of something unspecified.

  html/index.html          three-state card status, the footer
  tests/cards_check.py     new - 44 checks
  tests/aggregates_check.py  two assertions re-pointed (see below)

Done when
  [x] card status lines read clearly and use the canonical status colours
  [x] the footer is unambiguous about what it is showing
  [x] localization still functions - verified by switching language, saving,
      reloading and reading the value back off the server
  [x] admin.js:484-517 behaviour is unchanged - and the file is byte-identical

Two probes needed re-pointing, and both were asserting wording rather than
behaviour

  aggregates_check waited for the card's status line to be non-empty and then
  matched the phrase "SOP complete". The wait is now wrong for a second reason:
  the line is non-empty from the moment the page loads, because it says
  "Checking the SOP...". It waits for the answer instead, and matches the
  ANSWER rather than the sentence - which is what that check was ever about,
  since it exists to prove the answer came from the server and not the cache.

  A probe that breaks when wording changes is a probe that will be edited
  carelessly the next time wording changes. Both are now written so that only a
  behaviour change can fail them.

Verified one at a time
  cards_check    44/44  new
  aggregates     16/16  (two assertions re-pointed)
  browser_check  71/71
  a11y           22/22
  launcher       58/58
  f_items        F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added: the three status colours are --cds-support-success,
--cds-text-secondary and --wp-status-warning-text, all already in
theme-light.css.

Question for the PR, per CLAUDE.md: the footer now says work packages created in
the pilot are kept. That is true of the database and it is the thing people
actually want to know, but it is a promise, and whoever owns the pilot should
confirm it is one we are making.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:24:27 -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
6088ef17e8 T5.3 - B4 surface: a pipeline strip, four server counts, four shareable links
Four cells on the launcher - Work packages, Release ready, On hold, Overdue -
every number from /api/wps/metrics, which T4.1 built. There is deliberately no
cache fallback anywhere in renderPipeline(): a remembered number sitting beside
three live ones is the failure B4 exists to remove, wearing a different hat.

The cells are the four the endpoint already computes and the dashboard already
filters on, so they map 1:1 onto its flags rather than inventing a fifth slice
nothing downstream understands. wp-creation-app.js now names them (DASH_FLAGS)
for the same reason: a cell linking to a filter the board does not recognise is
a dead link that still looks live.

"Links to a filtered view via a shareable URL" needed the filter to BE URL state,
which it was not - the dashboard kept its flag in a variable. So:

  - dashToggleFlag pushes ?flag=<f>, and clears it on the way out of the board
  - the creator applies ?flag= before its first render, not after (applying it
    after paints the whole board and throws it away)
  - Back and Forward move through filters like any other state
  - work-package-suite-app.js forwards the flag ACROSS the iframe boundary, since
    the creator's src carries only the project. B7/T7.1 dissolves that frame and
    this hand-off goes with it; it is commented as such.

Zero is a real answer for one slice of a project that has work in it. Four zeros
on a project with none is not a reading, it is a strip that looks broken - that
case gets a sentence and a way into the creator instead. A failed request gets an
explicit error naming the failure, and no cells at all.

  html/index.html            the strip, its states, PIPE_CELLS
  html/wp-creation-app.js    flag as URL state; DASH_FLAGS; dashApplyFlag
  html/work-package-suite-app.js  forward the flag into the frame; clear on exit
  tests/pipeline_check.py    new - 43 checks

Done when
  [x] every number comes from a server endpoint - proved by poisoning localStorage
      with 99 fake packages and demanding the strip still read the server's 4
  [x] each cell links to a filtered view via a shareable URL - and the probe
      FOLLOWS the link and reads the filter inside the frame rather than trusting
      that a correct-looking URL was built
  [x] a project with zero work packages renders a sensible empty state
  [x] the strip announces updates via aria-live (polite - a count is not an
      interruption) and reports aria-busy while it is counting

What the probe caught

  The link landed on "Complete the SOP Configuration first". Not the strip's
  fault: browser_check.py's fixture stores a bare {governance: …} blob as the SOP
  data, where production stores {sop, state}. restoreSavedSOP() needs `state` and
  bails without it, so sopComplete stays false and the WP tab shows its gate.
  pipeline_check seeds the production shape. The underlying wart is real and is
  logged rather than fixed - see BL-018.

Verified one at a time
  pipeline_check  43/43   new
  launcher_check  58/58
  stepper_check   70/70
  url_state       23/23   the dashboard's new flag state did not disturb it
  aggregates      16/16
  browser_check   71/71
  a11y            22/22
  autosave        34/34
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added: still 0 across all page sheets and inline blocks. The
four cells are told apart by a label, a sentence and an accent - three channels,
so colour is not carrying it alone (C1).

Raised, not fixed
  BL-018  The WP tab's gate is the last localStorage-derived status in the app.
          T4.1 moved the launcher's card to the server; the wizard page still
          decides gate-or-creator from wp_suite_sop_complete plus a state blob.
          pullProject refreshes both on load so a connected user is fine, but the
          two answers come from different places and the fallback is silent.
          Includes a second, sharper edge: project-data.js:210 writes that flag
          for ANY row returned, including one with no `state` to restore - so the
          flag is written and never read consistently. T7.1 owns it.

Question for the PR, per CLAUDE.md: the strip counts Overdue against `data.due`,
which is free text today. CR-004/CR-018 restructure location but not dates. If
"overdue" is going to drive anything beyond a launcher tile, that field needs a
type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 10:38:58 -05:00
77c043c3db T5.2 - B3: a first-run empty state, then the picker card comes out
B3's warning is about ORDER, and it is the whole item: the proposal removes the
project-picker card, and the first-run empty state was built inside it. Remove
the card first and every brand-new account lands on a page whose only
instruction is to choose from a list with nothing in it.

So the empty state was built first, as its own thing rather than a branch inside
a control that is going away, and only then did the card go.

The launcher now shows exactly one of three states:

  no projects at all   what a project is for, the create form already open (it
                       is the only thing to do on this page, so hiding it behind
                       a button is one click of ceremony in front of the only
                       way forward), and the sample offered underneath it
  none chosen          point at the app bar's switcher, which is on every page,
                       plus New project
  one active           the tool cards, as before

Switching moved to the app bar's switcher entirely. Its popover footer used to
read "All projects / new project" and link to index.html - half of that promise
moved into the popover itself and the other half needs a form, so the link now
says New project and carries #new-project, which the launcher opens on.

The create form was rebuilt, so per C1 it ships accessible: a real <form> with
requestSubmit, every input labelled, and its validation inline at the field with
aria-describedby and role="alert" - the same shape T5.8 gives the wizard. That
retires the "Project name is required." alert (index.html 6 -> 5).

  html/index.html    three states, rebuilt create form, picker card removed
  html/wp-chrome.js  popover footer link (one line - it named the card)
  tests/launcher_check.py  new - 58 checks, two seeded databases
  tests/f_items.py   F1 rewritten to drive the controls that replaced the select

Done when
  [x] a brand-new account with zero projects sees a clear path to create one
  [x] the sample project remains discoverable from the empty state
  [x] the picker card is removed only after the empty state ships
  [x] switching projects still works from the header for users who have projects

Two things the probes caught that I would have shipped

  F1 went INCONCLUSIVE, not FAIL. Its probe drove `document.querySelector
  ('select')` on the launcher - the picker card's dropdown. It refused to guess
  rather than reporting a silent pass, which is the behaviour f_items was
  written for. Rewritten to drive both replacements, because they fail
  differently: the switcher RELOADS with ?project=<id>, so its two labels cannot
  drift apart whatever subscribes to what; creating a project changes the active
  project IN PAGE, and that is the interaction F1's mechanism actually applies
  to. It is now the only in-page change on the launcher, so it is the arm that
  matters. Both pass - the bar subscribes through ProjectData.onActiveChange.

  launcher_check reported "no focus ring" on the rebuilt form's inputs. That was
  trap 5 in reverse: without CDP focus emulation the headless document is not
  the focused one, :focus-visible never matches, and every control reports NO
  ring - a false red where a11y_check would get a false green. With emulation on
  they draw 2px --cds-focus from T4.7's app-wide floor.

Verified one at a time
  launcher_check  58/58   new (38 empty-account + 20 populated)
  stepper_check   70/70
  browser_check   71/71
  aggregates      16/16
  a11y            22/22   launcher 29 focusable elements, all >= 3:1
  url_state       23/23
  autosave        34/34
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added: 0 across all five page sheets and all seven inline
<style> blocks.

Raised, not fixed
  BL-014 updated rather than left stale: two of its four sites (.proj-row select,
  .link-like) went with the picker card, and the third (.proj-form-grid input)
  was measured rather than assumed - it draws T4.7's ring, which post-dates that
  entry. What survives is field.html's .fld-search, which T9.5 should measure the
  same way instead of inheriting the wording.

Question for the PR, per CLAUDE.md: with the picker gone, an account whose only
project is archived sees the choose-a-project prompt plus the archived note, and
the switcher lists nothing. That is honest but bleak. Whether an archived project
should stay switchable read-only is a product call, not an implementation one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 10:24:41 -05:00
b54034db04 T4.2 - S3: the app's state has an address; X1 is unblocked
Wave 0 counted pushState across html/ and found 0. Every page read its query
string once at boot and never wrote one again, so you could not send anyone a
link to WP07, a refresh dropped you back at the default view, and Back left the
app entirely because the app had never added a history entry.

CR-011 and CR-014 both promise an email carrying a direct link to a work package.
That is X1, and it was blocked on this. It is not blocked now.

html/wp-url.js is the whole mechanism, and it is deliberately NOT a router.
Nothing in it intercepts navigation or renders anything; it is the query string
treated as state that can be read, merged, written and subscribed to. Pages keep
their own rendering. Query parameters rather than a hash, because the server
already serves these paths and a hash is never sent to the server - which matters
the day a link has to be resolved before the page boots.

The merge behaviour is the part that earns its place: WPUrl.push({wp:id}) keeps
the active project, and WPUrl.push({wp:''}) clears one key without needing to know
what else is in the URL. Hand-built URLs losing ?project= is the usual way this
goes wrong.

WIRED: the creator (open package, dashboard view), the SOP wizard (tool, step),
the launcher (project). Each records a history entry only when the user chose the
change - restoring from the URL uses replace, or Back would immediately add an
entry and appear to do nothing.

WPUrl.absolute() is what CR-011/CR-014 will paste into an email in wave 8.

TWO BUGS THIS TASK CREATED AND FIXED, both found by the probe rather than by
reading:

  - bootSOP() calls newPackage() during boot, and newPackage() cleared ?wp=. A
    deep link therefore worked and then erased its own parameter, leaving Back
    with nothing to return to. Now guarded on wpCreatorReady.
  - goToStep() runs validateStep(), which ends in alert() when a required field
    is empty - always true on a freshly loaded page. So restoring ?step=3 from a
    shared link opened a modal dialog mid-boot, and hung the browser under CDP.
    Restoring a view is not a forward navigation and no longer runs the
    forward-navigation guard.

The second one is worth keeping in mind for the rest of wave 4: this app has 79
native dialogs, and any of them firing during a restore path will hang a headless
browser rather than fail visibly.

VERIFICATION. tests/url_state_check.py, 23 checks, all passing, covering every
done-when on the task:

  - a URL identifying a work package opens that package
  - the same URL for a SIGNED-OUT user goes to login, carries the target through
    ?next=, and lands on the work package itself after signing in
  - refresh preserves project, package, tab and view
  - Back and Forward move through states, verified as still-initialised rather
    than reloaded, and with the dashboard actually rendered rather than only the
    URL changed
  - a different user opening the same URL reaches the same view
  - nothing credential-shaped appears in the query string

Metric 8, pushState: was 0 at wave 0, now 2 in html/ (one pushState and one
replaceState, both in wp-url.js) behind 6 call sites across 4 files. The raw
count stays low by design - one place writes history, which is the same reason
the token work put one place in charge of colour.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:32:31 -05:00
b670ae719d T4.1 - B4: the counts come from the server, and disagreement is said out loud
The defect B4 names is not that the numbers were wrong. On one browser with one
cache they were right. It is that they were derived from the caller's own
localStorage, so two people on the same project saw different numbers and neither
was told.

SERVER. /api/wps/metrics now returns everything the creator's dashboard shows -
total, mine, release-ready, on hold, overdue, est/actual hours, by_status,
by_discipline, progress (overall and per discipline) and the gating list. It
already existed for a subset; the rest was being summed in the browser.

Two things moved to the server rather than being duplicated there:

  - PROGRESS_WEIGHT, the status-to-percentage table. It was PROGRESS_W in
    wp-creation-app.js; the JS copy is deleted rather than left in place, because
    two copies of a weighting table is how the two drift apart.
  - "release-ready" now counts `waitingOn` predecessors as blocking, which the
    browser did and the old endpoint did not. Without that the phrase would have
    changed meaning the moment the dashboard stopped computing it locally.

New GET /api/projects/{id}/summary gives the launcher the SOP state it was
reading out of localStorage.

by_location is shaped for CR-018 in wave 6, per the task's instruction not to
build a shape that cannot group by building/floor/sector. It reports its own
dimensions alongside the groups:

  {"dimensions": ["location"], "groups": [{"key": {...}, "total": n,
    "release_ready": n, "on_hold": n, "overdue": n, "by_status": {...}}]}

Today a package carries one free-text `location`, so that is the one dimension.
_location_key() already prefers structured building/floor/sector when present, so
CR-004 changes the dimensions and the keys and leaves the response shape alone.

CLIENT. The launcher's SOP card and the dashboard's tiles, chips, progress bars
and gating panel all read the server. There is deliberately no cache fallback: a
silently-stale number that looks authoritative is the thing being removed, so a
failed request renders an explicit error and a retry.

Writes flush through the outbox before the counts are re-read (dashRefreshAfterWrite).
Without that the refresh races the push and shows pre-write totals - the same
stale number arriving by a different route.

THE ONE COUNT STILL COMPUTED LOCALLY, stated rather than skipped: the board table
is a LIST of the packages this browser holds, which is what keeps the field view
working offline, and its header counts rows. Rather than pretend otherwise, it is
reconciled against the server's total and shows "this browser has N of M" when
they differ - usually a save that has not finished syncing. Nobody now sees a
number that disagrees with the project without being told, which is what B4 asks
for.

VERIFICATION. tests/aggregates_check.py, 16 checks, all passing. It tests what was
broken rather than whether the totals are right - the latter passed before this
change:

  - root and pat get byte-identical aggregates for the same project
  - the dashboard shows the server's total after localStorage is POISONED with a
    different package list; it cannot do that if it is summing the cache
  - a simulated outage renders "Counts unavailable", names the failure, offers a
    retry, renders no tiles beside it, and announces via role=alert
  - the launcher reports the SOP complete against a cache that says otherwise,
    and says "Could not check" when the request fails
  - by_location's groups are keyed by its declared dimensions, carry their own
    rollups, and sum to the project total

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:24:12 -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
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
d3d2a46922 T2.2 (part 1) - S4: mount the shared drawer on the launcher, SOP wizard and creator
PARTIAL. This is the mounting half of T2.2 and it closes S4. The other half -
deleting the flat nav strip, unifying the brand treatment, and removing the T1.2
interim - is NOT in this commit. See below.

S4 is that global nav was missing on the two pages people actually live in. The
drawer already handled everything needed to fix that: it self-mounts into
.wp-appbar OR .header, gates its links by role, carries the active ?project=,
and returns early inside an iframe. So this is two tags per page, not a port -
the component was built for this and was simply never included.

Verified on all 6 pages at 390px and 1440px: the menu button and the drawer
exist, both carry the same 8 links, the drawer opens fully on screen, its first
link is hit-testable via elementFromPoint rather than merely present, aria-hidden
flips correctly, and Escape closes it. Drawer links measure 44px tall, which is
the tap-target size T2.2 asks for on the Field View.

The embedded creator correctly does NOT get a second drawer: wp-sidenav.js
returns early when framed, and the SOP wizard hosting it already has one.

Deliberately NOT done here, and still open on T2.2:
  - the flat nav strip is not deleted, so the drawer and the strip currently
    duplicate Home, Admin, Users and Sign out
  - the creator's mono wordmark is not yet unified to the logo chip
  - the T1.2 interim is still in place and must be removed, not layered on
Splitting it this way keeps a working app at every commit: mounting is additive
and regression-free, whereas deleting the strip changes where several
destinations live and wants its own diff. T2.3 (B2 breakpoints) is untouched.

browser_check 71/71. f_items unchanged: F1-F5 FIXED, F6 untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 20:46:07 -05:00
5d5511a458 T1.1 - F1: one source of truth for the active project; the app bar subscribes
The hero, the picker and the create-user card showed the active project while
the app bar still read "Select a project". Three separate causes, all of them
the same shape - a reader with its own copy of the value.

1. Nothing told the bar. wp-chrome.js rendered projectLabel() once at build
   time and refreshed it only when /api/projects came back, so selecting a
   project updated the hero and left the bar behind. ProjectData.setActive now
   notifies, and the bar subscribes through ProjectData.onActiveChange instead
   of holding a copy. A plain array of callbacks - this is one value with a
   handful of readers, not a reason for a state library.

2. admin.html and users.html load wp-chrome.js but never loaded
   project-data.js, so window.ProjectData was undefined and their bar could
   NEVER show a project - it read "Select a project" permanently, whatever was
   selected. Both now load it, ahead of wp-chrome.js.

3. setActive({id}) erased the name. field.js, wp-creation-app.js and
   work-package-suite-app.js all set the id first and the full record second;
   writing that stub verbatim left the bar rendering "(unnamed)". setActive now
   merges onto the stored record when the id matches, so a partial write cannot
   lose fields it did not mean to touch.

Also: index.html never honoured ?project=<id>, though every other page does, so
a deep link on a browser with nothing stored showed "Select a project" while
the URL said otherwise. It now resolves the parameter before reconciling.

setActive is the only code path that writes wp_active_project /
wp_active_project_obj - project-data.js:83-105, noted there in a comment so it
stays that way. A storage listener keeps a second tab from showing a project
the user has since switched away from.

Verified, all at 1440px and against the wave 0 baseline:
  - bar shows the project on launcher, SOP wizard, admin, field, users
  - survives a hard refresh on each of them
  - selecting a project updates hero and bar in one interaction, no reload
  - with nothing selected the bar reads "Select a project" and both the
    launcher picker and the bar's own switcher are reachable
  - deep link ?project= works on a cold browser, hero and bar agree
  - setActive({id}) after a full record keeps the name

The creator is the one page with no app bar to fix: it loads neither
wp-chrome.js nor wp-chrome.css, because it renders as the iframe child of the
SOP wizard. Giving it chrome is T7.1's work once B7 dissolves that boundary -
adding it here would put a second app bar inside the embedded view. This is the
"all 6 pages" wording in the plan meeting the 7 pages that exist; see file-map
D1.

tests/f_items.py F1 now reports FIXED. F2-F6 still reproduce, untouched.
browser_check.py 71/71.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 18:13:32 -05:00
4ace2afb1c Move user administration to its own page; add Project Super User
User accounts lived in the Admin Console, which is admins-only. Project admins
need to create the accounts on their own jobs without an app admin on the phone,
so accounts move to a new User Directory page and a new role carries the right.

server/auth.py, server/app.py
  New permissions role `project_super_user`, between admin and project_admin:
  everything a project admin may do, plus user administration SCOPED to the
  projects they hold the role on. Four limits make it safe to hand out, all
  enforced server-side:

    * Scope comes from projects, not the job title. It resolves per membership
      (managed_project_ids), so an ordinary account can hold it on one job via
      ProjectMember.role, and a super user demoted on one job administers
      nobody there. No projects, no authority.
    * Account-level changes (password, disable, rename, permissions, delete)
      require EXCLUSIVE scope: refused when the target is also on a project the
      caller does not administer, because those changes are global. The
      directory renders such rows read-only with the reason.
    * No admin or super-user targets, and neither role can be granted by a
      super user -- that is the line that stops it becoming app-wide control.
    * PUT .../projects rebuilds only the caller's own slice; memberships on
      projects they do not administer are left untouched. A payload that simply
      omits them must not cut someone off a job the caller cannot see.

  Creating requires naming at least one of your own projects: an account with
  none would be one the creator instantly cannot manage.

  /api/auth/users is now scoped rather than admin-only, and carries a per-row
  `manageable` verdict plus the reason. Non-managers get a contact card only --
  a project user has no business reading colleagues' login history. New
  /api/auth/user-scope tells the page what it may offer. Administrative
  password resets are now audited; they were the one account change that left
  no trace. Settings, feature flags and the auto-add rule stay admin-only.

  While here: one definition of "is a user manager", derived from the managed
  set. An account-role-only version disagreed with the scoped one and locked
  per-project super users out of routes they were entitled to.

html/users.html, html/users.js
  The directory: three renderings from one page -- admin (everything), super
  user (controls per row, read-only where scope is shared), everyone else (a
  read-only directory of the people on their own projects).

html/console.css, html/console-util.js
  Extracted from admin.html/admin.js so both console pages share them. A
  divergent jsq() is an XSS and a divergent role list offers permissions the
  server refuses, so neither may exist twice.

html/wp-sidenav.{js,css}
  Global nav drawer, role-gated, carrying ?project= across links. Mounted on
  the field view (which had no way to anywhere) plus both console pages.

No migration: users.role is already String(20) and the new value fits.

Verified: 93 scope/gate tests, 29 live HTTP tests through the real dependency
stack, 33 static JS checks. Not verified in a browser -- no JS engine on this
machine -- so users.html and field.html want one manual load.

server/smoketest.py still fails with 401s. Pre-existing: it has no login code,
so auth_gate refuses it. Confirmed unchanged by stashing this work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 17:36:14 -07:00
928ab8c900 Archive projects, auto-add default members, rebuild the admin console
Three things asked for together, plus the migration they share (a7c31f9e5b02 —
additive, with database defaults for existing rows, so unlike the users.role
rewrite it is safe under a code-only rollback).

ARCHIVE A PROJECT. A finished job leaves every picker, switcher and search, and
freezes read-only, without losing anything. Hiding is free: GET /api/projects
defaults to archived=exclude, so the home picker and the app-bar switcher drop it
without either of them changing. Freezing is require_project_writable(), which
every write that lands on a project now goes through — SOP and WP upserts (both
ends, so a package can be moved neither into nor out of an archived job), deletes,
issue, status, WP archive, and comments on its WPs/SOPs. It answers 409, not 403:
nobody lacks a permission, the project's state is the objection, and the browser
outbox in project-data.js retires 4xx ops instead of retrying them against a job
that will never accept them. Unarchive and delete stay allowed on purpose —
unarchive is the one write an archived project must take, and archive-then-delete
is a normal sequence.

DEFAULT MEMBERS ON NEW PROJECTS. users.auto_add_projects / auto_add_role flag the
people who belong on every job, so an admin says it once instead of remembering it
at each project creation. It runs on the is_new branch of upsert_project, which is
the single road into project creation, so the home page, the sample project and the
demo seeder are all covered and an update never re-runs it. Note the interaction
with the existing creator-grant: that row commits first and add_default_members
never overwrites an existing membership, so the creator grant now carries the
creator's own auto_add_role — otherwise someone flagged "Project Admin on every
job" would land as a plain member on the one job they started themselves.

ADMIN CONSOLE. The user table had outgrown .wrap{max-width:860px}: nine columns in
an 860px card meant every cell wrapped, so one user occupied a ~100px band, the
action buttons stacked, and the table spilled outside its own white card. Now
1240px, with wide tables scrolling inside .tscroll so the page itself never scrolls
sideways, and one spacing/control scale across all twelve cards. Truncation hangs
off a span inside the cell rather than max-width on the td, which table-layout:auto
treats as advisory — the usual reason cell ellipsis works in the stylesheet and not
on the page.

Found in review and fixed here rather than later:

- Stored XSS in the new Projects card, reachable by any signed-in user, landing in
  an admin's session. The uesc(v).replace(/'/g,"\'") idiom this file already used
  in eight places escapes in the wrong order — uesc leaves backslashes alone, so a
  stored name containing \' closes the JS string literal and the rest executes.
  jsq() does backslash, then quote, then HTML, and all thirteen handler bindings go
  through it. The same bug, unescaped entirely, was in the SOP builder's custom
  constraint names (escHandlerArg there). Three of seven test payloads escaped the
  literal under the old idiom — one of them a plain name ending in a backslash, so
  it was breaking buttons for innocent input too.
- _save_comment resolved wp_id and sop_id with if/elif but stored both, so a
  payload naming a WP you may touch and a SOP you may not was authorised on the WP
  alone and still wrote into the other project's thread. Both are checked now.
- Promoting an account to admin left its default-member flag set but invisible,
  ready to take effect again on demotion — cleared, as set_user_auto_add already
  does for the role.

smoketest.py and the console's own smoke test both assert the archive round trip:
out of the default list, present with archived=all, writes refused with 409, and
all of it undone by unarchiving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:48:43 -07: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
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
afdc815fb4 Store SOPs and Work Packages in the DB (shared across users)
Reintegrates C-West8's "storing data in DB instead of client only"
(commit e5102446 on shared-data) on top of the BIM / per-package work.
localStorage becomes a per-browser cache; the server is authoritative.

- project-data.js: pullProject() hydrates the apps' existing localStorage
  keys from the API on load; pushSOP()/pushWP()/removeWP() write through
  on save/delete. WPs store the whole flat object in `data`, so BIM
  fields, kind, and projectLinks round-trip intact.
- index.html: home pulls the project before showing SOP status; feedback
  loads from /api/comments (server-authoritative, local fallback).
- work-package-suite-app.js: pull-then-restore on boot; completeSOP
  pushes the SOP to the server.
- wp-creation-app.js: save/duplicate/issue/setStatus push; delete/clear
  remove; boot pulls from the server first, then boots off the cache.
- server/app.py: /api/sops and /api/wps take full=true to return the
  data JSON for one-request hydration (list stays lean by default).

Co-Authored-By: C-West8 <125926137+C-West8@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:58:33 -07:00
20afb0e565 Add secure username/password login portal
Gate the suite behind a self-contained login (no external IdP):

- User model with bcrypt-hashed passwords; admin/user roles
- /api/auth endpoints: login, logout, me, change-password, and
  admin-only user management (list/create/delete/reset/enable)
- Stateless JWT session in an HttpOnly, SameSite=Lax, auto-Secure
  cookie; middleware refuses every /api data route without a session
- login.html + auth-guard.js: login page and per-page guard with a
  top-right "name / Admin / Sign out" pill
- Admin Console now gated on admin role (passphrase gate removed) with
  a User administration card
- manage_users.py CLI to bootstrap the first admin
- Rebuilt help.js into a searchable, multi-topic help center
- Local-dev convenience: app serves html/ so the site + API share one
  origin under uvicorn (inactive in the prod container)
- Docs/env: AUTH_SECRET_KEY, requirements (bcrypt, PyJWT), README

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:55:54 -05:00
c010bc22a0 Update home footer to BTG / Pilot Use Only
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:40 -07:00
e3ef3b0023 Round-1 test feedback + smoke-test script
- API smoke test (server/smoketest.py): stdlib end-to-end check of health,
  projects, SOPs, WPs, the AWP issue gate (409 → 200), status, metrics,
  comments, and cascade delete. Referenced from DEPLOYMENT.md.

SOP config:
- Constraints: fix custom constraints never appearing — renderStandardConstraints
  no longer clobbers state.constraints; customs render in their own list with
  remove buttons; modal gains a free-text "Add" field.
- Sources: add column headers (Data Type / Location-Platform / URL / Notes);
  preset data types are now fixed labels, "Add Source" creates an editable
  custom row.
- Issuance strategy: add a tooltip + worked examples for each option.
- Remove the "Comment submitted" acknowledgement popup (home + suite); keep the
  commenter name between comments.

WP creator:
- Clearing the last open constraint now offers to mark the package Issued and
  scrolls to the status control.
- Form sections are collapsible (click a section heading to fold it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:04:46 -07:00
e5f77846ad GUI polish: Help section, tooltips, sticky save bar + section nav, dashboard filters
- Help: shared help.js injects a Help modal (workflow + key concepts) and the
  .help-tip tooltip component. " Help" added to the suite header and home nav.
- Tooltips: ⓘ hover hints on the trickiest fields (WP number auto-build,
  disciplines, scope/split, constraints, materials-by-discipline, discipline
  strategy, split threshold).
- WP creator: sticky section-nav jump chips at the top and an always-visible
  sticky save bar (Save Draft / Save & View) showing live release readiness.
- Dashboard: metric cards (Release-ready / On hold / Overdue / Total) and the
  status chips are now clickable filters for the board.
- Consistent colored status pills in the dashboard board and the saved list.

Theme unification (home Carbon vs tools palette) intentionally left for a
separate pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:37:01 -07:00
e5c450597a Isolate SOP/WP data per project (namespaced localStorage)
SOP and Work Package localStorage keys are now namespaced by the active
project via ProjectData.key(base) -> base+'__'+<projectId> (SK() in the
suite, wpKey() in the creator), so switching projects shows that project's
own SOP and packages. project-data.js runs a one-time discard of the legacy
un-namespaced keys (guarded by wp_ns_migrated_v1), per the chosen approach.

Active project is resolved before the store loads in both the suite and the
creator so namespaced keys resolve correctly, including standalone deep-links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:46:59 -07:00
3c40b58ff8 Add multi-project support: projects entity, picker home page, project context
Projects become the top-level container; SOPs and Work Packages belong to one.

Backend:
- New projects table + CRUD (/api/projects).
- sops.project_id (FK, cascade) and work_packages.project_id added;
  list/latest/metrics endpoints accept a project_id filter.

Front end (now under html/):
- project-data.js: shared API-first ProjectData adapter with localStorage
  fallback + active-project helpers.
- Home page: removed "About This Suite"; added a Project picker (create /
  use sample / select). Tool cards stay hidden until a project is active and
  carry &project=<id>; hero shows the active project.
- Suite reads ?project, resolves it, shows it in the header, and prefills the
  SOP project fields; passes &project into the WP-creator iframe.
- WP creator stamps projectId onto saved packages.

SOP/WP localStorage is not yet namespaced per project (next step).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:39:45 -07:00
4d111d608d Merge branch 'main' into feat/wp-discipline-split-dashboard 2026-06-15 15:28:35 -07:00
fd668f0ea2 Move static files into html/ for Docker bind mount 2026-06-15 14:47:24 -05:00