6088ef17e8efc5389dda0a8672d1b34acf715f8c
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 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>
|
|||
| 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>
|
|||
| 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>
|
|||
| 79b0e955b4 |
Wave 1: permissions roles, account-backed SOP team, critical constraints, password reset, BIM flag
Acts on the site comments from 8/3 plus the follow-ups. Foundation work first — four of the comments all needed the project team to resolve to real user accounts. Permissions vs project role (new) - User.role is now the PERMISSIONS role: admin | project_admin | project_user. project_admin may delete work packages, change a SOP after it is complete, and delete a project; project_user may not (archiving a WP is still open to them). Enforced by require_project_admin() server-side; the UI only hides dead ends. - New User.project_role holds the person's JOB FUNCTION on the project. It grants nothing — it feeds the SOP team pickers and notification routing. - Admin console shows both columns and explains the difference. Migration rewrites the legacy role 'user' to 'project_user'. - Deleting a project was previously open to any member and unaudited; it now needs project_admin and writes an audit event. ProjectData.remove no longer drops the project from the local cache when the server refuses. SOP project team from user accounts - PM/APM/CM/QM and additional team members are pickers over the project's members, storing the account id next to the display name. A name from an older SOP with no matching account is kept and flagged rather than dropped. - The WP Creator lists the SOP team first in the Owner picker, and a new package defaults to whoever is creating it. Critical constraints - SOP constraints carry a Critical flag; buildConstraints() now copies the whole definition through to the package (it previously reduced them to names, losing description too), and critical rows are marked in the WP form. The email on reopen-after-release is wave 3. Password reset by email - login.html gains Forgot password and a set-a-new-password view, offered only when the server reports email is actually configured. - Single-use signed token (AUTH_RESET_MINUTES, default 60) bound to token_version, sent immediately rather than through the notifications outbox so a reset link is never persisted. Identical response for unknown accounts; per-account send cooldown; a completed reset clears any login lockout. - Session and reset tokens are no longer interchangeable. BIM kill-switch - New admin Features card with bim_enabled, OFF by default. The SOP creator hides the BIM section and the Creator treats every package as install-only while it is off; a SOP that already has BIM keeps its data untouched. Verified with two throwaway-database test scripts: 44 checks on the permissions matrix and token handling, 22 on the reset flow end-to-end against a local SMTP sink (real message captured, link extracted and used). Front-end files parse-checked in headless Chrome. Not yet exercised in a browser against a real login. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 39b48055ff |
Productionize WP Suite: auth, security hardening, sync, dashboard, PWA, email
Brings the Work Package Suite from a browser-local prototype to a multi-tenant, SQL-backed deployment hardened for customer IP. Auth & access control - Local username/password login (bcrypt + JWT in an HttpOnly cookie), admin-managed users, per-project membership, and project-scoped API access. - Admin console: change user roles, view the audit trail, manage settings. Security hardening - CSP / HSTS / X-Frame-Options / nosniff headers in nginx; Secure cookie via X-Forwarded-Proto; CSRF Origin check; attribute-safe output escaping. - Login lockout, token_version session revocation, stronger password policy, fail-closed secret loading, encrypted (AES-256) database backups. Persistence & schema - SOPs and Work Packages are now DB-backed and shared across users, written through a durable client sync outbox that queues offline edits. - Alembic migrations applied automatically on container start. New capabilities - Phase 2 dashboard (progress, gating, pagination, archive). - Phase 3 PWA "Field View" with offline caching and auth fallback. - WP owner assignment with OPTIONAL email notifications, OFF by default and toggled from the admin console. SMTP password is read only from the SMTP_PASSWORD env var (never stored); emails carry a WP number + deep link, never customer IP. Also: IBM Carbon restyle, Help section, and DEPLOYMENT.md brought up to date. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| afdc815fb4 |
Store SOPs and Work Packages in the DB (shared across users)
Reintegrates C-West8's "storing data in DB instead of client only"
(commit
|
|||
| 5ad3ffa58e |
Per-project access control + UI/feedback/admin refinements
Access control: - project_members table; non-admins only see/operate on assigned projects (enforced across projects, SOPs, work packages — 403 else), admins bypass. Creating a project auto-grants its creator access. - Admin API to get/set a user's project assignments, plus a checkbox assignment dialog in the Admin Console user list. UI / workflow: - Login page: drop the "Prime Controls" wordmark next to the logo. - SOP tool: remove emoji icons from buttons and nav tabs. - Rename "Step Comments" to "Feedback"; the author auto-populates (read-only) from the signed-in user. - Move usage-log viewing to the Admin Console; add an admin card that lists all feedback/comments (who, what, page + step, when). - Sample project name -> "Micron FMCS Install (sample)". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| e5c450597a |
Isolate SOP/WP data per project (namespaced localStorage)
SOP and Work Package localStorage keys are now namespaced by the active project via ProjectData.key(base) -> base+'__'+<projectId> (SK() in the suite, wpKey() in the creator), so switching projects shows that project's own SOP and packages. project-data.js runs a one-time discard of the legacy un-namespaced keys (guarded by wp_ns_migrated_v1), per the chosen approach. Active project is resolved before the store loads in both the suite and the creator so namespaced keys resolve correctly, including standalone deep-links. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| 3c40b58ff8 |
Add multi-project support: projects entity, picker home page, project context
Projects become the top-level container; SOPs and Work Packages belong to one. Backend: - New projects table + CRUD (/api/projects). - sops.project_id (FK, cascade) and work_packages.project_id added; list/latest/metrics endpoints accept a project_id filter. Front end (now under html/): - project-data.js: shared API-first ProjectData adapter with localStorage fallback + active-project helpers. - Home page: removed "About This Suite"; added a Project picker (create / use sample / select). Tool cards stay hidden until a project is active and carry &project=<id>; hero shows the active project. - Suite reads ?project, resolves it, shows it in the header, and prefills the SOP project fields; passes &project into the WP-creator iframe. - WP creator stamps projectId onto saved packages. SOP/WP localStorage is not yet namespaced per project (next step). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |