ae30c58337cd79146fae9729801fbc8ef44c54ab
101 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 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>
|
|||
| 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>
|
|||
| 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>
|
|||
| 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>
|
|||
| 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>
|
|||
| 6fbc5b9735 |
Wave 4 exit - record the new probes and move the baseline counts
Wave 4's five exit criteria, and what shows each of them: counts come from the server, identical across users aggregates_check 16/16 work packages addressable by URL, incl. signed-out url_state_check 23/23 drafts autosave, unsaved work guarded, save honest autosave_check 34/34 toasts announce, helper text legible, focus visible a11y_check 22/22 X1 and X5 cleared see below X1 is cleared: a work package has an address, and WPUrl.absolute() is what CR-011 and CR-014 will paste into an email in wave 8. X5 is cleared: /api/wps/metrics returns by_location with its dimensions declared alongside the groups, so CR-004 and CR-018 can group by building/floor/sector in wave 6 by changing the dimensions and the keys, not the response shape. Four metrics moved off their wave 0 baselines and are recorded in the file map so wave 9 measures against the right numbers. The one that matters most is not in the plan's list: `outline: none` went from six to one, and the survivor has its replacement in the rule directly above it. The file map also now says to run the browser probes ONE AT A TIME. Chained, they exhaust the headless browser's ports and abort with "browser would not start", which reads as a code fault and is not one - it cost a confusing red run here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 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>
|
|||
| 0cce0b4191 |
T4.4 - B5: two save indicators, two sentences, neither readable as the other
The app already showed "✓ All changes saved". That badge belongs to the OUTBOX -
it reports whether saved records have reached the project - and it went green
when the queue emptied, whether or not anything in the form had been saved at
all. So the promise B5 says the app does not keep was being made by a component
that could not know whether it was true.
Two indicators now, each speaking for one thing:
DRAFT .wp-draft-status, mounted in the creator's sticky save bar and the
wizard's step navigation. Driven by WPAutosave's status: "No unsaved
changes" / "Unsaved changes" / "Saving draft…" / "Draft saved at HH:MM"
/ "Draft not saved on this device — <reason>" with a Retry.
OUTBOX the existing badge, reworded so every state names the project:
"Sending N changes to the project…", "Everything sent to the project",
"N changes not yet sent to the project — retrying", "rejected by the
project".
"No unsaved changes" rather than "Saved" for an untouched form: those are
different statements and only the first is true before anything is typed. The
component was getting that wrong in the same way the outbox badge was.
Announced per S10 (T4.5's pattern, arriving one task early because this indicator
needs it to exist): role="status" while things are going well, swapping to
role="alert" on failure. A failed autosave means the safety net is not there, and
waiting for a pause in the screen reader's queue to mention that is too late.
The retry button is only rendered in the failed state - a retry offered when
nothing has failed is a button that does nothing.
Styles live in theme-light.css because both form pages mount the same component,
and a second copy in a page sheet is what wave 3 spent itself removing.
VERIFICATION. tests/autosave_check.py grew to 34 checks, all passing. The B5 ones:
- the indicator reports "No unsaved changes" untouched, then a real save with a
timestamp, and is visually distinct in each state
- a simulated storage failure is visually distinct, names the reason, offers a
retry, and switches to role=alert
- the sync badge no longer RENDERS "All changes saved", and every state it does
render names the project
That last check is deliberately scoped to what the badge renders rather than to
the file text: the old phrase still appears in the comment explaining why it was
changed, and asserting on that would be asserting that the reason cannot be
written down.
Note for wave 9: the outbox badge is styled with inline hexes, including #8a6d00
- the ninth amber from BL-009, independently confirming that entry. It is
BL-005's territory, not this task's.
browser_check 71/71.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| ab3c9e52d1 |
T4.3 - S2: autosave, an unsaved-work guard that means it, and draft recovery
The work package form is ~4,700px tall and had no autosave and no unsaved-work
guard. The only beforeunload listener in the app was analytics dwell tracking, so
a mis-click, a closed tab or a crash lost everything typed since the last Save.
html/wp-autosave.js separates three things this app was conflating:
THE DRAFT what you have typed. Saved locally, continuously, by this file.
THE RECORD what you explicitly Saved, which goes to the project.
THE OUTBOX project-data.js, which gets the RECORD to the server reliably.
This module owns the first only and never writes to the server. A draft is
"unfinished work this browser is holding for you"; pushing unfinished work into a
shared project is a different feature with different consequences.
The guard fires only when the form differs from what was loaded. "Do not fire the
guard when nothing has changed" is in the task because a dialog that appears on
every exit gets clicked through within a day, and is then worse than no dialog.
WIRED: the creator's package form and the SOP wizard's state. Both autosave on a
1200ms debounce, on section/step change, and on visibilitychange - the last being
what makes recovery survive a killed tab, since a crash never fires beforeunload.
The wizard's guard is ADDED alongside trackStepDwell, not in place of it; both
fire and the analytics one does not preventDefault.
THREE BUGS FOUND WHILE BUILDING THIS, all by the probe rather than by reading:
- Dirtiness cannot be "does the form match savedPackages". Those records come
back from the server through serverToPkg() in a LEANER shape - 264 characters
against the form's 1,820 - so a freshly loaded, untouched form differed from
its own record and every single exit would have prompted. Dirtiness is now
measured against a baseline snapshot taken when the form is populated.
- currentView is 'Work Package Form', not 'Form'. My first guard compared
against 'Form' and therefore returned false always: autosave was wired,
registered, and quietly dead. T4.2 had also introduced currentView='Form' in
its popstate handler; that is fixed here too, since it would have broken this
and anything else keyed off the view.
- settled() has to cancel the pending debounce. A save follows typing, so there
is nearly always a write already scheduled; without cancelling it the write
lands a second later and resurrects the draft that was just settled - and the
next load offers to recover work that is already saved.
VERIFICATION. tests/autosave_check.py, 23 checks, all passing:
- typing autosaves unprompted; the draft holds what was typed; it is scoped to
project AND package; and it does NOT appear in the outbox
- an untouched form is not dirty and arms no guard; a typed-in one does
- the draft survives a killed tab and is OFFERED back rather than applied
silently, saying plainly that nothing reached the project, via role=status
- restoring puts the work back in the form
- an explicit save settles the draft, and the probe asserts the save actually
landed first - otherwise the rest of that section proves nothing
- a simulated QuotaExceededError is reported as 'failed' with its reason, not
swallowed; a silent autosave failure is a safety net that is not there
- trackStepDwell still records an event
Two notes for later waves. The fixture's SOP defines no WP types, so
savePackage() legitimately refuses until the probe supplies one - worth knowing
before someone reads that as a bug. And native dialogs hung the headless browser
twice more in this task; with 79 of them in the app, any restore or save path
that reaches one will hang a test rather than fail visibly. S6/S7 in wave 9.
browser_check 71/71, f_items 5 FIXED / F6 REPRODUCES, url_state 23/23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 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>
|
|||
| 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>
|
|||
| 12c0e5ca74 |
Wave 3 exit - record what the wave actually changed on screen
wave-3.md asks for "no unintended visual change on any page versus wave 2".
Measured with tests/token_check.py,
|
|||
| 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>
|
|||
| 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> |
|||
| 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> |
|||
| 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> |
|||
| 0e40e967a0 |
T3.1 - C3/S5: inventory the token systems, and correct the accent baseline
Produces docs/reference/tokens.md. No stylesheet is touched; T3.1 is inventory. What the inventory found that the plan did not say: - It is six stylesheets plus the launcher's inline <style>, not four (D1 again). 202 custom-property declarations, all listed with file and line. - The wave 0 accent baseline is wrong: 15 declarations across 5 sheets, not 14 across 4. console.css:13 packs five declarations onto one line and the baseline's `^\s*--` regex only ever matches the first, so console's own --accent was never counted. Corrected command is in tokens.md section 10. The wave 9 target of one sheet is unchanged; there is one more to remove. - Three mono stacks, not two. The file map recorded console.css dropping ui-monospace and Segoe UI Mono; wp-chrome.css:159,221 is a third stack that drops Cascadia Mono and Segoe UI Mono. - --shadow-lg does not differ by blur, as the file map says. Both are 0 4px 16px. The difference is the colour: rgba(0,0,0,.16) against rgba(20,30,50,.12). That means they can be unified later with no layout consequence at all. - Twelve var() fallbacks can never fire, because the token they fall back from is declared at :root on a sheet the page loads. Free deletions for T3.2. - --shadow: none is a no-op token with 8 consumers. Left for T3.3, which is hunting exactly this class of silent nothing. - 111 var() references live in .js files across 23 token names. A rename there fails silently - no build error, no console warning, just an unstyled element. Section 9 is the list to grep before deleting any alias. - There is a second brand blue: #2563d6, filling .sop-inherited at 7% alpha on every field a work package inherited from its SOP. Logged as BL-008. The document states one rule up front, because it is the difference between a clean wave 3 and a broken one: consolidation is not unification. Where two sheets declare the same value, T3.2 collapses them. Where they declare different values for the same role - the two banner greens, the three error borders, the two shadows - each value gets its own canonical name and the pair is recorded. Picking a winner between two near-identical greys is a visual change, which T3.2 forbids. Section 8 computes the near-duplicates rather than eyeballing them. The one to watch is the zebra stripe: console's #fafafa sits six points from #f4f4f4, and collapsing them erases the striping on the nine-column user table. New backlog entries: BL-004 (help.js ships 52 colours in a different design language), BL-005 (two modals styled entirely by inline style= attributes), BL-006 (17 half-pixel font sizes), BL-007 (--radius: 0 contradicted 45 times in the sheet that declares it), BL-008 (the second blue). One decision T3.2 needs and this task cannot make: adopting the superset mono stack changes the rendered face on machines that have Segoe UI Mono or ui-monospace but not IBM Plex Mono, which is most of the target environment. That is a real change on admin and users. Either accept it and re-shoot those two baselines - capturing twice, since they are not byte-stable - or keep console.css's narrower stack as a second token until T3.5. Written up in tokens.md section 6d and 8-H; built to neither until it is answered. Verification: f_items 5 FIXED / F6 REPRODUCES as expected, browser_check 71/71. Screenshots not applicable - this task changes no rendered surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| b105135a98 |
T2.3 - B2: a breakpoint plan for the project switcher, not an ellipsis
"Micron EUV Cleanroom Enable 2667008" did not fit. The switcher capped the name
at 240px inside a 280px button, so the real project name was ellipsised to
"Micron EUV Clean..." on the one control whose job is to tell you which job you
are in - which is the failure this item exists to prevent.
The plan, now written into docs/reference/file-map.md so it is not re-litigated:
>= 1024px the full project NAME. Caps raised to 400px / 340px, which is
enough for a real name with no ellipsis.
< 1024px the project NUMBER alone, e.g. 2667008. Dropping the name beats
shortening it: short, stable, unambiguous.
The number is no longer prefixed to the name at wide widths. Real names already
end with their number, so the old format printed it twice - the test project
rendered as "2667008 - Micron EUV Cleanroom Enable 2667008".
The full name stays reachable at every width by three routes, none hover-only,
because the Field View is a touch surface:
- the drawer, which now shows the active project under its head, wrapping onto
as many lines as it needs and never truncated. This is the guaranteed route,
and it subscribes to ProjectData.onActiveChange so it does not go stale when
the launcher switches project without reloading.
- the switcher's title attribute, carrying the full number and name, which
surfaces on keyboard focus as well as hover.
- the switcher popover, which lists every project in full.
Crossing the breakpoint re-labels, so a resized window does not keep whichever
form was chosen at load.
Verified with the real long name at 390, 768, 1024 and 1440: nothing truncated
at any width (scrollWidth vs clientWidth, not by eye), and the drawer carries
the whole name at all four. T1.1 correctness holds - the switcher never reads
"Select a project" while a project is active.
f_items: F1-F5 FIXED, F3 still passing at all four widths with the wider
switcher. browser_check 71/71.
Wave 2 complete: one drawer on six pages, no flat strip, F2 resolved for real,
S4 closed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 05aa67ff32 |
T2.2 (part 2) - B1/S4/F2: delete the flat strip, unify the brand
Completes T2.2. The flat strip was #wp-usermenu, built in auth-guard.js and appended to every bar: "Root . Admin . Users . Language & time . Password . Sign out". It duplicated Admin, Users and Sign out from the drawer, and being one unbreakable 412px run it was also the thing that clipped the bar at 390px. Deleting it is what resolves F2 for real, exactly as the wave predicted. Its two unique items moved into the drawer rather than being lost with it: Language & time and Password now sit under an Account section. They open dialogs rather than navigating, so they render as <button data-action>, not links - which also removes three fake `<a href="#">` entries whose only job was to be clicked. The creator's jump-link count goes 3 -> 0 for that reason. The drawer closes before the dialog opens, so its scrim is not left over the thing you just asked for. wpPreferences and wpChangePassword are untouched, so A7's localization feature is intact - it moved, it did not go. buildUserMenu, addLogoutPill, isDarkBg and the floating-pill fallback are all gone; grep finds no reference to any of them. The fallback is not needed because every signed-in page now mounts the drawer. The T1.2 interim went with them rather than being layered on - the element it wrapped no longer exists. Also removed the Field View's "Home" link, which the drawer's first entry and the brand beside it both already offered, and replaced the creator's mono wordmark with the shared logo chip. The creator was the only page in the suite spelling "Prime Controls" out in monospace instead of showing the mark; its .header-logo rule and the 480px override for it are deleted. Deliberately kept: the launcher's Overview / Feedback / Help links. Those are in-page anchors and a help dialog belonging to that page, not global navigation, so they are not the strip and the drawer does not duplicate them. Verified on all 6 pages at 390px and 1440px: no #wp-usermenu anywhere, no bar control crossing the viewport edge at either width, the logo chip present on every page including the creator, and the same 10-entry drawer everywhere. The bar is back to 48px at 1440px on five of six pages. F2's probe reports the bar at 2 rows instead of 4. Both migrated actions verified to open their real modals (wp-prefs-modal, wp-pw-modal) with the drawer closed behind them. browser_check 71/71. f_items: F1-F5 FIXED, F6 untouched. T2.3 (B2, switcher breakpoints) is still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 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>
|
|||
| 0a080be1f8 |
T2.1 - B1 part 1: the drawer is already one shared component; verified, not rebuilt
No code change. T2.1 asks to lift the drawer out of "three near-copies" into one shared implementation. There are no copies to lift. html/wp-sidenav.js and html/wp-sidenav.css are already a single implementation included by admin, field view and directory. .wp-sidenav, .wp-navscrim and .wp-navbtn are each declared in exactly one file, and no page defines its own drawer. The review's sentence was that the drawer "ships on 3 of 6 pages while a flat strip duplicates it" - the duplication is between the drawer and the flat strip, not among three drawers. The plan read "3 of 6 pages" as "3 copies". It already satisfies every T2.1 done-when, including the one that could have failed: activeProjectId() at wp-sidenav.js:62-68 reads ?project= and falls back to ProjectData.getActiveId(), which is the single source T1.1 established. It also self-mounts into .wp-appbar OR .header and returns early inside an iframe, so it is already built for the pages T2.2 has to put it on. Refactoring a single shared component into a single shared component would be churn with regression risk and nothing gained, so nothing was touched. Recorded as file-map D5 so the next reader does not go looking for the copies either. The real work of this wave is T2.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 357712e93e |
T1.6 - S13: seed_demo.py can seed a running instance again
Every /api/ route but /api/health requires a session and the script sent none, so it could not seed anything. It predates the commit that taught the smoke test to sign in. It now signs in the same way, reusing smoketest.py's build_opener rather than growing a second cookie-jar implementation - one login flow, one place to fix. Credentials come from WP_SEED_USER / WP_SEED_PASSWORD, falling back to WP_SMOKE_USER / WP_SMOKE_PASSWORD so one set serves both scripts, and it signs out in a finally. No bypass, no debug flag, no unauthenticated seeding route: the diff touches server/seed_demo.py and nothing else, adds no route decorator anywhere, and the 33 get_current_user dependencies in app.py are untouched. The script authenticates like a client; the server is not weaker than it was. Two things found while fixing it: The failure mode was worse than a refusal. call() swallowed the HTTPError and returned the error body, so a 401 surfaced as a KeyError on proj["id"] three lines later - which reads like a broken stack rather than a missing session. Writes now go through expect(), which stops on the first refusal and prints the status and detail. Running it twice used to print a note that scrolled past and then create a second identical DEMO project, leaving two of everything with no way to tell them apart. It now refuses, names what exists, and prints the --clean command. Also corrected the header's own instructions, which said the seeded SOP and Work Packages would NOT render in the UI because the front end still read them from localStorage "pending Phase 2 wiring". That stopped being true when the sync layer landed. Selecting the seeded project now shows 7 Work Package cards in the Field View, so anyone using the UI to check whether seeding worked is no longer told to expect nothing. Verified against a freshly started instance: no credentials aborts cleanly with exit 2 and no traceback; a first run exits 0 and seeds a project, a complete SOP and 9 packages; a second run exits 1 without duplicating; the data is visible in the picker, the hero, the app bar and the Field View; --clean removes it and exits 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 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> |
|||
| 4d3258113a |
T1.4 - F4: the comments drawer opens below the header, not under it
Containing block first, as the task asks. The drawer is a body child with no
transformed ancestor, so its containing block was already the viewport - the
positioning context was never wrong. What was wrong was `top: 0` with
`height: 100vh`: the drawer started at the very top of the viewport, and the
creator's .header is sticky with z-index:100 against the drawer's 61. The
header won, so the drawer's own head - its title and its close button - was
roofed over and unreachable. It read as "off-screen" because the part you
needed was covered, not because the box had escaped the viewport.
That is why raising z-index would have been the wrong move: it does not remove
the collision, it just swaps which element is on top, and then the drawer
covers the header instead. The fix is to stop them occupying the same band.
The drawer now starts at var(--rail-top) and is that much shorter. --rail-top
is the header's measured height, set by wp-creation-app.js:1328 and already
used by .wp-nav for exactly this purpose, so "below the header" has one
definition on this page rather than two.
The iframe boundary is NOT implicated. position:fixed inside the embedded
creator resolves against the iframe's own viewport, which is self-consistent,
and the drawer behaves identically framed and unframed. T7.1 can dissolve the
boundary without revisiting this.
The probe was checking one width, one mode, and placement only. It now checks
390 and 1440, standalone and embedded, that the close button is genuinely
hit-testable via elementFromPoint rather than merely present, that the drawer
reopens after closing, and that opening it does not move the page's scroll
position. All pass.
One honest caveat, attributed rather than hidden. At 390px the drawer sits at
the right edge of a 485px layout viewport while the screen is 390px, so 95px of
it is off-screen. That is not the drawer: the creator forces its containing
block to 485px, and while chasing it I found BL-001's root cause -
wp-creation-app.js:1389 injects `body{--nav-w:288px}` with no media query,
which lands after wp-creation-styles.css:815's
`@media (max-width:860px){body{--nav-w:56px}}` and overrides it, so the page
reserves 288px of rail that is not there at any width. Every `right: 0` fixed
element on the page is displaced by it, not only this one.
Left unfixed on purpose - it is the creator's layout, T7.1 rebuilds it, and
CLAUDE.md is explicit about not fixing things noticed in passing. BL-001 now
carries the exact cause and the five rules that consume the token, so T7.1 does
not have to find it again. The probe reports it as an attributed note naming
BL-001, so nobody is sent to the wrong file.
browser_check 71/71. f_items: F1, F2, F3, F4 FIXED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 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>
|
|||
| 440f3239a4 |
T1.2 - log the two backlog entries the commit message referenced
BL-001 updated: its 1440px half was resolved as a side effect of the F2 fix, not by intent. Left open, scoped to the creator at 390px, so T7.1 still checks it. BL-003 added: user-menu links are 16px tap targets. T1.2 made them reachable; it did not make them comfortable. Deferred to T2.2, which replaces the markup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 0f6f91ce2c |
T1.2 - F2 (interim): the app bar no longer clips at 390px
INTERIM. T2.2 is the real fix: wave 2 replaces this markup with the existing drawer. Nothing here is meant to survive that, so it is the smallest change that makes every control reachable, not a redesign - no hamburger, no responsive menu, no avatar dropdown. The bar already wrapped at 720px, so the wrap rule was not the problem. The problem was #wp-usermenu, built in auth-guard.js with an inline white-space:nowrap on the container: "Root . Admin . Users . Language & time . Password . Sign out" became one unbreakable 412px run inside a 374px bar. Being inline and unclassed, no stylesheet media query could reach it. At 390px that put "Sign out" at x382-432 - half of it past the edge, exactly as the review described. The container now wraps and each link carries nowrap instead, so "Language & time" still breaks as a unit rather than mid-phrase. Bar scrollWidth at 390px goes 424 -> 374, and "Sign out" moves onto its own row, fully visible. The truncated search is the other half of F2. The control was always usable - what was cut was the placeholder - so below 620px, the breakpoint wp-chrome.css already uses for this element, it reads "Search..." instead of "Search work packages, projects, SOPs...". Verified at 390px on all 7 pages: no bar control crosses the viewport edge, and "Sign out" is fully within it everywhere. At 1440px the screenshot diff against the wave 0 baseline is byte-identical for login, launcher, SOP wizard and field view. Three pages differ, all intended: admin and users because T1.1 gave their bar a project to show, and the creator because this change removed its horizontal overflow. That last one is worth flagging: the same unbreakable menu run was the cause of four of the five overflows recorded in wave 0, including BL-001, the creator scrolling sideways at 1440px. Overflow at capture is now 1 of 14 shots rather than 5 - only the creator at 390px remains, which is its own layout and is T7.1's to resolve. BL-001 is updated rather than closed, so T7.1 still checks it. Tap targets in this menu are 16px tall. Not touched here - it is C1's, audited in wave 9 - and logged as BL-003. f_items F2 FIXED. browser_check 71/71. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| d22834f2f1 |
T1.1 (cont.) - F1: fill in the id-only stub, and resolve deep links in the bar
Completing T1.1. My first verification primed localStorage before loading each
page, which made both sources of truth agree and hid two remaining cases. Re-run
with genuinely cold storage, the app bar still showed "(unnamed)" on the field
view and "Select a project" on the console pages.
Two causes, both the same F1 shape - a page holding a copy the shared store
does not have:
1. field.js could only write {id} at boot (it needs the id synchronously, for
the per-project storage namespace), then resolved the full record into a
local PROJECT variable, rendered "Project: Job A" from it, and never
published it. The store kept the stub, so the bar read "(unnamed)".
setActive now fills a nameless record in from the cached project list, or
from the API when the cache has not loaded yet, and re-checks the id before
applying a slow response so it cannot overwrite a project the user has since
switched to. That fixes every caller of this shape rather than the one that
was caught - work-package-suite-app.js and wp-creation-app.js write the same
stub. field.js also publishes the record it already fetched, so the common
path costs no extra request.
2. admin.html and users.html have no project-resolution logic of their own, so
nothing read ?project= and a deep link left the bar on whatever was last
stored. The bar is the one component every chromed page has, so it resolves
the parameter once in wp-chrome.js rather than being taught to five pages.
Verified with localStorage cleared before every navigation: a cold deep link
now shows the project on field, SOP wizard, launcher, admin and users, and the
stored record carries the name rather than a stub.
The creator remains the one page with no app bar - it loads no chrome because
it renders as the iframe child. T7.1.
browser_check 71/71. f_items F1 FIXED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 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>
|
|||
| fe8a27e022 |
T0.2 - baseline captured; all six rendering defects confirmed present
Runs the app from a clean database, captures the before images, and records which of F1-F6 actually still reproduce. All six do. Rather than eyeball screenshots, each defect is measured in a browser by tests/f_items.py, which reports REPRODUCES / FIXED / INCONCLUSIVE and never a silent pass. That makes it both the wave 0 record and the wave 1-3 regression check: an item is done when its probe flips to FIXED. F1 hero says "Job A", app bar still says "Select a project", no reload F2 "Sign out" spans x382-432 in a 390px viewport - cut in half, 3 rows F3 chrome paints over the logo by 106x32px; .header-left collapses to 0 F4 comments drawer overlaps the header by 380x91px in the standalone creator F5 5 of 5 ENABLED wizard inputs compute #f4f4f4 on #e0e0e0 F6 11 cards in one 5,017px scroll, 0 tabs (review said ~4,700px; it grew) Three probes needed care to avoid reporting a false pass, and the traps are worth knowing before anyone verifies a fix: F1 disappears if localStorage is primed first, because then both sources of truth agree. The probe clears it and drives the real picker. F3 needs a long project name that is long IN THE DATABASE - any page reached with ?project= re-pulls it and overwrites a locally-faked one. It also cannot be measured by comparing .header-left to the chrome: under the long name .header-left (flex:1, min-width:0) collapses to clientWidth 0, so that comparison reports a tidy zero gap while the chrome paints across the logo. It measures against .logo, which is flex-shrink:0. My first two attempts at this probe both reported FIXED for those reasons; the screenshot did not. F5 must ignore genuinely disabled inputs or a fix looks done while real fields stay grey. 14 screenshots, not the 12 the plan asks for, because there are 7 pages (file-map D1). Capture also measures horizontal overflow, which is how BL-001 was found. Tooling: cdp.py gains viewport() and screenshot() - it could do neither, and T0.2 requires 390px and 1440px images. 390px sets the mobile flag rather than just narrowing the window, since every page declares width=device-width and Chrome otherwise lays out at 980px and no media query under test fires. Both new scripts reuse browser_check.py's seed() and start_server() instead of growing a second fixture. Existing browser_check still passes 71/71. No application code changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| c2e35b9261 |
T0.1 - build the file map and verify the plan's line references
Wave 0 exists because the plan's line numbers came from a review of users/directory-super-user rather than a fresh read. This is the fresh read: 7 pages, 6 stylesheets, 11,867 lines, with each page's stylesheets, scripts and iframe role recorded, and all seven baseline counts captured with the command that produced them. Four discrepancies, one of which matters a great deal: D2 - CLAUDE.md's "logged-override path for predecessors stays (A1). See wp-creation-app.js:1962-1972" cites the wrong function. That range is dashIssue(), which REFUSES to issue and says "open the package to release it early with a logged reason". The reviewer read that sentence and correctly inferred an override exists, but cited the mention rather than the code. The audited path is confirmEarlyRelease() at 967-984 plus seven satellites (state at 392, call sites at 998 and 1149, persisted at 1117, rendered at 1215, rehydrated at 1674, reset at 481/488/1744). A T7.3 that preserved only 1962-1972 would delete the business rule while believing it had protected it. D1 - "6 pages, 4 stylesheets" is 7 and 6; wave-0's own parenthetical lists seven names. Every "all 6 pages" done-when is off by one. D3 - four documents the plan reads from are deliverables not yet written. D4 - the creator overflows horizontally at 1440px, which no F item covers. Logged as BL-001 rather than fixed, since T7.1 rebuilds that layout anyway. BL-002 records that outline:none appears three times in the wizard sheet, not once, so T3.4 fixes all three. Counts confirmed against the review: 79 dialogs (43 in the creator), 12 div and 2 span onclick, 15 help-tip badges, 0 aria-live, 0 pushState. The "4 declarations of #0f62fe" needed a definition - there are 31 occurrences and 14 custom-property declarations; the 4 is the number of stylesheets declaring their own accent token, which is the number wave 9 should drive to 1. No application code changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 3d99d4b9d0 |
Import the R2 implementation spec into the repo
The plan was delivered as wp-suite-implementation-spec.zip and lived only in Downloads, so every "read CLAUDE.md first" instruction in it pointed at a file the repo did not have. Bring it in unchanged: CLAUDE.md, IMPLEMENTATION.md, and docs/waves/wave-0 through wave-9 plus backlog.md. UX-REVIEW-2026-08-14.md is committed alongside it. It is the review that produced F1-F6, S1-S13 and the A/B/C assessments, and item IDs throughout the wave files cite it, so it belongs under version control rather than sitting untracked in the working tree. No application code changes here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 9459e76a6c |
Add a front-end browser check, so the pages are testable and not just readable
server/smoketest.py proves the API works; nothing proved the PAGES work. That gap
is why users.js, wp-sidenav.js and the extracted console.css shipped unexecuted and
had to be written up as a known issue instead of verified. This closes the gap with
a tool rather than a one-off, so the next front-end change is cheap to check.
tests/cdp.py a minimal DevTools Protocol client — hand-rolled stdlib
WebSocket (handshake, masked frames), browser discovery for
Edge/Chrome across platforms, and process teardown.
tests/browser_check.py the fixture and 71 assertions.
Stdlib only, matching smoketest.py's rule: these have to run on a plain Python
install on whatever machine is to hand. No pip, no Selenium, no node.
Self-contained — it builds a throwaway database, seeds a fixture, starts its own
uvicorn on a free port, drives the browser, and tears everything down. The real
database is never touched. Sessions come from minting a token with the app's own
auth.create_token() rather than scripting the login form.
What it asserts, beyond "no JavaScript errors on boot" (the thing that actually
went unverified): the three role-dependent renderings of the directory, one-line
rows and no sideways scroll, the roles each caller may grant, the project-access
dialog opening and closing, the drawer's open/Escape/scrim/focus/aria behaviour and
its role gating, ?project= carried only onto project-scoped links, and — the reason
this matters most — that admin.html still has its tokens, cards, headings and dense
sticky tables after console.css was lifted out of its inline <style>.
Three things the build had to get right, each learned the hard way:
- Teardown kills the browser's whole process tree AND sweeps anything still
holding the unique temp profile, matched on that path so a browser window the
user has open is never touched. proc.kill() alone left 98 strays.
- Launching retries with a fresh profile and port: a browser can hand off to
another instance and exit rc=0 without ever binding the debugging port.
- Cleanup waits for the server to exit and disposes the harness's own SQLAlchemy
engine before removing the temp directory, or the open SQLite file blocks the
delete and ignore_errors hides it.
The fixture includes an account on a project the super user cannot see, without
which the admin and the super user would see the same number of rows and the
scoping assertion would prove nothing.
Documented in DEPLOYMENT.md next to the smoke test. 71/71 across repeated runs,
leaving no stray processes or temp directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 7f831bf1ca |
Close the browser-verification gap: the front end has now been run
Deletes known issue 3. users.js, wp-sidenav.js and the extracted console.css had
never been executed, because there is no node/deno on the machine they were written
on. Edge is, so the pass was driven through the DevTools Protocol with a hand-rolled
stdlib WebSocket client, signing in by minting a session with the app's own
auth.create_token() rather than scripting the login form.
70 checks, twice, all passing — covering the five steps that entry listed:
1. users.html as an admin: 9 columns, one-line rows, no sideways scroll, all four
grantable roles, the project-access dialog opening and closing on Escape, and
your own permissions cell locked to a tag while your job function stays editable.
2. As a Project Super User: banner naming the project, only in-scope accounts
listed, out-of-scope rows read-only with the reason on hover, and exactly the
two roles they may grant.
3. As an ordinary project user: 6 columns, no create form, zero controls, emails
still reachable as mailto links.
4. field.html: drawer opens, closes on Escape and on the scrim, aria-expanded and
aria-current correct, focus moves inside, 44px tap targets, Admin Console hidden
from non-admins, and ?project= carried onto project-scoped links only.
5. admin.html: console.css loaded, --ctl resolving, cards and headings and sticky
dense tables intact after the extraction, user administration gone and replaced
by a link, and the admins-only gate still holding for a non-admin.
Every page boots with no JavaScript errors, which was the actual unknown.
Two things the pass surfaced, neither a defect: a 404 on /api/sops/latest is the
API's designed answer for a project with no SOP ("No SOP found") and the browser
logs every 4xx, so the fixture now seeds one; and role pills only appear where a row
is rendered read-only, since an editable row shows a dropdown instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 64a5fd5612 |
Make the smoke test sign in; enforce SQLite foreign keys
Closes known issue 3. server/smoketest.py predated the login portal and had no
login step at all, so auth_gate refused every route after /api/health and the
documented way to verify a deploy reported a wall of failures against a healthy
stack.
- Signs in first, holding the session in an http.cookiejar on a shared opener.
urlopen() has no cookie support, which is why the session was dropped.
- Credentials from WP_SMOKE_USER / WP_SMOKE_PASSWORD, or --user/--password, so
a password need not land in shell history. Refuses to start without them
rather than running headlong into 401s.
- Checks the signed-in role up front and warns when it cannot archive or delete
a project, instead of failing six checks later for an unexplained reason.
- New exit code 2 for "could not run" (unreachable, or credentials missing or
rejected), kept distinct from 1 "ran and found problems".
- Also asserts the session is accepted on an authenticated route and refused
after sign-out; signs out at the end so a run on a shared host leaves none.
The working smoke test immediately caught a real bug: SQLite ships with foreign
keys disabled and the pragma is per-connection, so every ondelete="CASCADE" was
silently a no-op on dev while working on Postgres. Deleting a project orphaned its
SOPs, work packages and membership rows; deleting a user orphaned theirs. db.py
now sets PRAGMA foreign_keys=ON for SQLite, so dev matches production.
Enforcing them exposed two things that had been getting away with it:
- create_user adds an account and its ProjectMember rows in one flush, and the
ORM takes flush order from relationship() declarations. models.py has none by
design, so it emitted the child INSERT first and the database rejected it.
Fixed with a db.flush() after the account, and documented at the top of
models.py so the next same-flush pair does not rediscover it. The other three
call sites already commit the parent first.
- A write aimed at a since-deleted project used to leave an orphan row; with FKs
enforced it would have been an IntegrityError surfacing as a 500, which the
browser outbox retries forever (it only retires 4xx). require_project_writable
now refuses a vanished project with 409, like the archived case beside it.
Verified: smoke test 27/27 exit 0 against a live server (the cascade assertion now
passes on SQLite, which is what used to fail); credentials missing and credentials
rejected both abort cleanly with exit 2 and no stray PASS lines; a project_user run
warns up front and fails as described. Scope tests 93/93, live HTTP checks 29/29,
static JS checks 33/33. No orphan rows left in the database afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| c99ef08cf1 |
Record the smoketest auth gap and the browser-verification gap
Two things surfaced while building the User Directory that are worth a decision rather than a mention in a handover. 3. server/smoketest.py has no login step, so auth_gate 401s every check after /api/health. DEPLOYMENT.md presents it as the way to prove the stack works end-to-end, including a docker compose exec invocation, so the documented verification path reports failure on a healthy system -- the failure mode most likely to be believed. Rated Medium for that reason. Predates the login portal; confirmed unrelated to this branch by stashing it and re-running. The Admin Console's in-browser smoke test is the working equivalent today. 4. users.js and wp-sidenav.js have never been executed -- no JS engine on the machine they were written on. Logged as a verification gap, not a defect, with what WAS checked (server tests, delimiter balance, handler resolution, id targets) and what only a browser can settle (layout, transitions, focus trap). Includes the five-step manual pass that closes it, and the hard-reload note, since sw.js bumped to wp-suite-shell-v6 and a soft reload serves the old shell. Both entries follow the file's existing shape: what is wrong, what it costs, why it is still open, what closing it takes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 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>
|
|||
| 3cccdf1c4b |
Merge branch 'docs/deploy-runbook': project archiving, default members, admin console rebuild
Brings in the 2026-08-05 work plus the deploy runbook and KNOWN-ISSUES.md. Carries migration a7c31f9e5b02 (additive, with server defaults). No overlap with the entrypoint/backup-script changes already on main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 153fe97a31 |
Record the SOP discipline-name XSS as a known issue
It is a real hole and we are shipping without fixing it, so it needs to be written down somewhere that outlives the conversation it came up in. Discipline names are rendered into inline handlers in the WP creator escaped with esc(), which maps ' to '. That is right for text and wrong here: the browser decodes entities in an attribute before the JS parser sees it, so the entity becomes a bare quote and closes the handler's string literal. Escaping for a handler argument has to go backslash, then quote, then HTML — esc() only does the last part. Same bug, same ordering, as the two fixed on 2026-08-05 (jsq() in admin.js, escHandlerArg() in work-package-suite-app.js); this one predates that work and sits in a file it did not touch. Left open rather than fixed because the suite is internal, behind a login, with named employee accounts and no anonymous input path — the likely cost is a discipline named "Owner's Equipment" silently breaking its own pill, not an attack. The entry records the conditions that change that judgement (exposure outside the corporate network, accounts for subcontractors or clients, self-registration), so the rating cannot go quietly stale if the deployment story changes. Neither CSP nor the CSRF gate mitigates it, and both are noted so nobody re-derives that hopefully. Also records the archived-project rough edge from the same day: the server refuses writes with 409, but the two big apps still present Save and Issue buttons, so the failure is safe but late. data-wp-archived is already on the document element for whoever closes it. Each entry says what closing it takes, and entries get deleted in the commit that fixes them — otherwise this file becomes a museum instead of a queue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 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>
|
|||
| 7bb1f66588 |
Merge pull request 'Add entrypoint fallback for missing bind-mounted scripts' (#2) from scripts/entrypoint into main
Reviewed-on: #2 |
|||
| 26f4a9242a | Entrypoint static variable removed to allow validation of scripting files before startup | |||
| 6c1dc7d45f |
Add entrypoint fallback for missing bind-mounted scripts
- scripts/entrypoint.sh (new): prefers the live bind-mounted backup-cron.sh, but falls back to a copy baked into the image at build time if the mount is missing. If neither exists, it stays up and idle (instead of crash-looping) so the container remains reachable via console/exec for diagnosis. - scripts/backup-cron.sh (updated): resolves db-backup.sh the same live-or-fallback way, re-checked on every loop iteration, so if the bind mount comes back healthy later, this container picks up the live scripts on its next backup run with no restart needed. - scripts/backup.Dockerfile (updated): bakes all three scripts into the image under /app/scripts-default/ as the fallback, and sets the new wrapper as ENTRYPOINT. |
|||
| e5977758c0 |
Hand-off runbook for the 2026-08-04 deploy
A step-by-step deploy procedure for someone who administers the Docker host but does not know this app. Two things about this deploy need spelling out for them, and neither is obvious from DEPLOYMENT.md: - nginx's config and all of html/ are baked into the image, so the stack has to be re-pulled and re-built. A restart deploys nothing and looks like a success. - the pending users.role rewrite (b41c7ae9) is one-way as far as the app is concerned: rolling the API image back after it commits breaks logins, because the old code doesn't recognise 'project_user'. So the runbook records `alembic current` and both image IDs up front, and splits rollback by symptom — an nginx-only failure is a safe code-only rollback they can do alone, a failed migration is escalate-don't-improvise. Backups go through the existing sidecar rather than an ad-hoc pg_dump: it works from Portainer's console without SSH, writes an encrypted timestamped dump to backups/ on the host, and prints a success line worth checking. Commands use `docker exec <name>` throughout, since `docker compose` from an SSH session can't find a Portainer-managed stack's compose project. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| a9b22f2add |
NGINX: set Cache-Control via a map, not a nested location, so security headers survive
The Cache-Control rule I added in the previous commit used a nested `location ~* \.(html|css|js|webmanifest)$`. nginx does NOT inherit add_header into a block that declares its own add_header, so every HTML, CSS and JS response would have been served WITHOUT the CSP, HSTS, X-Frame-Options, Referrer-Policy and nosniff headers from the Phase S hardening — the headers dropped for exactly the files that matter most, and silently, since the pages would still work. Now computed by `map $uri $wp_cache_control` at http level and applied with one server-level add_header alongside the security headers, so nothing is scoped away. An empty value makes nginx omit the header entirely, so images and fonts stay cacheable. Applied to both the Docker config (nginx/conf.d/wp-suite.conf) and the bare-metal one (nginx-wp-suite.conf), which carries the same header set. Caught while checking whether the stack was safe to redeploy. Not verified with `nginx -t` — this machine has neither nginx nor docker — so DEPLOYMENT.md now records the rule and the one-line curl that confirms both headers are present after a deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 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> |
|||
| 917a728399 |
Rebuild the work-package side panel in the style of MS Planner
The auto-hiding drawer was the wrong model — a list you navigate by shouldn't appear and disappear under the pointer, and its vertical text tab read as a stray artifact. Replaced with a persistent side panel following the Planner reference: - Collapse toggle at the top (the panel glyph, arrow flips), remembered across visits. Collapsed leaves a 56px icon rail where the coloured package badges are still clickable, rather than hiding the list entirely. - One primary action: "+ New work package" with a split caret for Duplicate, Split by discipline and Export all. - Icon nav with counts: My packages (owned by you), All packages, Needs attention (on hold or not release-ready), Dashboard. These filter the list below. - Packages as rows with a colour-coded initial badge, number, subject and readiness state, still grouped by status, with a left accent bar on the current package. The badge colour is hashed from the WP number, so a package keeps its swatch instead of shuffling when another is added or deleted. - The panel sits IN the layout: the form and the full-width chrome shift beside it rather than being overlaid. Also, the reason it appeared as loose unstyled widgets in the middle of the form: the panel's markup and its stylesheet are cached independently, so a browser can run new markup against old CSS. Its essential layout (fixed position, width, the row/badge flex, the collapsed rules) is now injected by wp-creation-app.js as a floor, inserted first in <head> so the stylesheet still wins on everything it defines. Same lesson as the iframe: a component whose CSS-missing state is "broken" rather than "plain" must carry its own critical layout. Verified with 25 driven checks in headless Chrome: persistence, the four nav links, badge colours and text, view filtering, collapse/expand, the split menu, row selection and highlighting — and, with wp-creation-styles.css removed from the page entirely, the panel is still a fixed 288px side panel with the form shifted beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 40bd19b6cf |
Stop the embedded creator collapsing to a 300x150 box; serve code network-first
The Creator rendered as a tiny double-scrolling square in the WP tab. My fault, and the mechanism matters more than the symptom: I had moved the iframe's sizing (width:100%, border:0, min-height) out of its inline style attribute and into work-package-suite-styles.css. The service worker cached the HTML and the stylesheet as INDEPENDENT entries, cache-first — so a browser could hold the new HTML together with the old CSS. With the inline sizing gone and the new rule absent, the iframe fell back to the HTML default 300x150 box and the whole tool collapsed. Moving self-contained markup into a separately-cached file created that window; nothing about the layout itself was wrong. Three layers so it cannot recur: - The iframe's width/border/min-height are inline again, on purpose, with a comment saying why. An iframe with no intrinsic size has a catastrophic failure mode, so its sizing must not depend on another file being in step. - applyEmbedLayout() now sets the fill height and width as INLINE styles via sizeWPFrame(). Inline beats any stylesheet, including a stale cached one, so the class is a refinement rather than a requirement. - sw.js: HTML/CSS/JS are now fetched NETWORK-FIRST with the cache as offline fallback; images/icons/manifest stay stale-while-revalidate. These files reference each other, so a page must never run against a stale sibling — this same staleness had already masked two other fixes during development. Cache bumped to v4. Verified: at 2560x1440 the tool spans the window with a single scrollbar; with work-package-suite-styles.css removed entirely (strictly worse than stale) the frame still measures 1469x662 instead of 300x150, and re-running the layout pass keeps it there; 12 checks across sop -> wp -> dashboard -> sop confirm body.embed-full, the content-area class, the fill class and the inline height are all cleared on the way out, so the wizard never ends up unscrollable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 6c3098922f |
Field view: a package waiting on a predecessor is not "Ready"
The field list judged readiness from open constraints alone, so a package whose predecessor isn't Closed showed a green Ready pill even though the server would refuse to issue it. It now shows "waits on N", matching the form, the dashboard and the navigator drawer. A deleted predecessor still doesn't block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 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> |