55caefb0992912b754b4aa582beb358de707c4c3
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 55caefb099 |
T5.8 - S1 (wizard): errors at the field, and the last dialog is gone
S1 has two halves and they are easy to conflate.
One is that validation was a native dialog: "Please complete all required
fields: Project Name, Number, Client, Division, and Site Location." names five
fields at once, highlights none of them, and scrolls nowhere. The other is that
validateStep guarded steps 1, 5 and 6 while the MARKUP marks required fields on
1, 3, 5, 6 and 7 - so two steps' asterisks meant nothing at all, which is worse
than no asterisk.
Both fixed, and the second is the one worth measuring: the probe reads the
required-field list out of work-package-suite.html rather than out of STEP_GATES,
because a probe that read the table would agree with whatever the table says and
prove nothing. Both notations count - an asterisk in a <label>, and the red span
beside step 3's role titles.
Now: an error per FIELD, rendered at it, associated by aria-describedby, marked
aria-invalid, announced through role="alert", and the first one focused and
scrolled into view. The error boxes are BUILT from the gate table rather than
written into the markup twelve times - adding a required field is one row, and
its error element, its association and its announcement all follow. A
markup-side error box somebody forgets to add is an error nobody ever sees.
An error clears as you type rather than on the next submit. An error still
showing over a field you have just corrected teaches people to ignore errors.
And nothing paints a step you have not tried to leave: the rail asks
stepGateMet(), which reads the same fields and marks none of them.
The thirteen dialogs
Every one is now the thing it should have been - an error at the field it is
about, or an announcement in a live region with the role T4.5 established:
errors interrupt, confirmations do not.
A dialog is not merely ugly. It blocks the page, cannot be placed or styled, a
screen reader can present it only as a modal interruption, and it is one OK
button whatever it says - so "sample data loaded" and "you cannot do that"
arrived identically.
Two deserve naming. The empty-comment alert became an inline error on the
feedback textarea. And showAnalytics() was a confirm() carrying the entire
usage summary as its body - a wall of text in a dialog whose only dismissal
was also the download button. The summary is the useful part, so it is shown,
with the download offered as an action beside it. That function has no caller
in the wizard's markup (the "Usage data" button is the creator's, calling the
creator's own showAnalytics), and it was converted rather than deleted:
deleting a feature is not what this task was asked to do, and its dialog
counted toward the number this task has to drive to zero.
html/work-package-suite.html #wp-toast, an error box on the textarea
html/work-package-suite-app.js STEP_GATES widened; per-field messages;
ensureErrorBoxes; wizardToast; 13 removals
html/work-package-suite-styles.css .wp-toast
tests/validation_check.py new - 81 checks
tests/stepper_check.py its "the alert T5.8 still owns" check now
asserts the opposite, by name
Done when
[x] every step with required fields validates them - 5 steps, from the markup
[x] each error renders at its field and is associated via aria-describedby
[x] submitting an invalid step focuses AND scrolls to the first error
(scroll checked by bounding box, not by trusting scrollIntoView)
[x] errors announce to screen readers
[x] the wizard's native dialog count is 0
The count, recorded both ways because BL-017 says the metric counts prose
work-package-suite-app.js 0 raw, 0 with comments stripped
app-wide 64 raw, 64 stripped, against wave 0's 79
wp-creation-app.js 43 (wave 7), users.js 10 and
admin.js 6 and index.html 5 (wave 9)
The wizard contributes none of what is left, which the probe asserts rather
than leaving to the total.
Verified one at a time
validation_check 81/81 new
stepper_check 71/71
sections_check 88/88
browser_check 71/71
a11y 22/22
url_state 23/23
autosave 34/34
locations 58/58
aggregates 16/16
pipeline 43/43
launcher 58/58
f_items F1-F5 FIXED, F6 REPRODUCES (T7.2)
No colour literal added. The toast says "error" by a red rule AND by staying
until dismissed where a confirmation times out - two channels, not one (C1).
Question for the PR, per CLAUDE.md: step 3's two role TITLES are validated
because the markup marks them required, but the two role NAME pickers beside
them are not marked and so are not gated. A sign-off role with nobody in it is
arguably the more useful thing to catch. The markup is what was built to; if the
intent was the names, that is two rows in STEP_GATES.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 3bc3abe209 |
T5.7 - CR-016: Assets off for Micron EUV, by configuration
Asset content duplicates the database Clinton's team maintains, and that
integration is deferred, so the section comes out in the interim.
This is a configuration task, and the wave file says so - "Surface:
configuration". There is no Micron EUV SOP in this repository; it is data on a
running system. What the repository holds are two representations of the Micron
configuration, and both now carry the toggle:
wp-creation-app.js SAMPLE_SOP what the standalone creator shows when
there is no project SOP to read
work-package-suite-app.js loadSample the Micron FMCS SOP the wizard loads
For a real project the mechanism is step 12 and needs no code: untick Assets,
complete the SOP. That is what T5.5 built and what "another SOP can enable
Assets and it works" means.
Both write a PARTIAL map - {assets:false} - rather than a full one.
WPSections.normalize fills the rest in as ON, so a section added after today is
not silently off for this SOP. The probe checks that specifically: exactly one
key is false, not nine true and one false frozen at today's list.
html/wp-creation-app.js SAMPLE_SOP.sections
html/work-package-suite-app.js loadSampleData sets state.sections
tests/sections_check.py +13 checks (75 -> 88)
Done when
[x] Assets does not render for Micron EUV in the form, detail view or export -
and not in the section chip strip either, which is the form's contents page
[x] the section and its model remain in the codebase - #asset-card and
pkgAssets are both still there, checked in the browser rather than assumed
[x] another SOP can enable Assets and it works - the fixture project renders it
seed_demo.py was deliberately NOT changed. Its SOP row stores a bare
{governance: ...} blob rather than the {sop, state} shape pullProject reads, so
its data never reaches the creator's SOP object at all - adding sections there
would be a toggle nothing consults. That shape mismatch is BL-018 and it should
be fixed as one thing, not worked around here.
What the probe caught
The first run reported on "Job A" while claiming Micron. activeProjectId falls
back to ProjectData.getActiveId(), so a project left in localStorage by an
earlier navigation sent bootSOP() down the real-project branch and the sample
was never loaded - every CR-016 assertion was then measuring the wrong SOP and
four of them happened to fail, which is the only reason it was noticed. The
probe clears storage first and says why.
Verified one at a time
sections_check 88/88 (75 + 13 for CR-016)
browser_check 71/71
f_items F1-F5 FIXED, F6 REPRODUCES (T7.2)
Question for the PR, per CLAUDE.md: CR-009's decision also says Kitting ships
toggled off for Micron EUV via these same toggles. That is wave 8's to apply
(CR-009/CR-010/CR-012), so the sample still has Kitting on. If the intent was
for both to go off together, the second line is one word.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| c453e50412 |
T5.6 - CR-002: Acumatica cost code and task, hidden by toggle
The team concluded these two are noise on a field work package: cost codes are
effectively constant on a job and the Acumatica task mapping is a PM concern.
The cost visibility they actually want is by building and floor, which is CR-004
and CR-018.
Hidden, not removed. CLAUDE.md: "Removed fields are hidden, not deleted (CR-002,
CR-016). Retain the data and the model." So this is a second, narrower toggle
list beside T5.5's sections - two fields inside General Information rather than
two more sections, because a section is a block of the document and these are
two rows in one.
no migration the values live in the work package's JSON data blob,
which nothing here writes to. The probe greps every
migration for a drop_column touching either.
no model change server/models.py is untouched by this task
no code change to the toggles are SOP data. Another project turns them
re-enable back on from step 12 and both fields return, values
included
A field is on only if its own toggle is on AND the section holding it is. Asked
as one question (WPSections.fieldOn) so no caller has to remember to ask both -
a field showing inside a hidden section is not a state worth reasoning about,
and the probe checks that case explicitly.
html/wp-sections.js FIELDS, fieldOn, normalizeFields
html/work-package-suite-app.js field rows nested under their section
html/work-package-suite-styles.css .field-toggle
html/wp-creation-index.html ids on the two .field wrappers
html/wp-creation-app.js WP_FIELD_NODES; both document rows conditional
tests/sections_check.py +22 checks (53 -> 75)
Done when
[x] neither field appears in the form, detail view or PDF export when off
[x] existing records still hold their values - a package EDITED while both are
off comes back through collectPackage() with both intact
[x] the fields can be re-enabled for another SOP without a code change
[x] no schema migration drops data - checked against every migration in the
tree, not just the ones this wave added
The whole .field wrapper is hidden, not the input: a bare label over nothing is
worse than either state.
Raised, not fixed
BL-019 A cost code that has left COST_CODES is silently blanked on edit.
wp_cost is a <select>, and setting .value to something with no matching
<option> does nothing at all - so opening such a package clears the
field and the next save writes the blank back. The same bug was fixed
once already for gov_wosize (work-package-suite-app.js:490-495) by
adding the stored value as an option; cost code never got it.
Found the honest way: a probe here used an invented cost code to prove
hiding a field does not delete its value, and the value came back
empty. That looked exactly like the toggle eating data. It was not, and
the probe now uses a real code and says why in a comment - a probe that
fails for a reason other than the one it names is worse than no probe.
Verified one at a time
sections_check 75/75 (53 + 22 for CR-002)
browser_check 71/71
stepper_check 70/70
a11y 22/22
url_state 23/23
autosave 34/34
locations_check 58/58
Question for the PR, per CLAUDE.md: BL-000b asks whether General Information
wants per-field toggles generally. This is not that - it is the two fields
CR-002 names, and the list is deliberately closed. If a third field wants one,
that is the general question and it needs the product answer BL-000b is holding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 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>
|
|||
| 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>
|
|||
| 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> |
|||
| 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>
|
|||
| 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> |
|||
| 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>
|