server/app.py:
- okta_login(): validates and stashes ?next= (same-site path only) in the
OAuth-state session before redirecting to Okta, so a deep link an assignment
email carried (X1/CR-011/CR-014) survives the round trip instead of always
landing on /index.html.
- okta_callback(): reads that stashed next= back (re-validated on the way out
too - belt and suspenders against a crafted value) and redirects there on
success. The two failure paths that used to raise a raw HTTPException -
OAuthError (sign-in cancelled/failed) and a locally-disabled account - now
redirect to /login.html?error=... instead: this route is reached by a full
page browser navigation from Okta, not a fetch() call, so a JSON error body
just looks like a broken page to whoever is signing in.
html/login.html + html/login.js: rebuilt as a single "Sign in with Okta" link,
replacing the username/password form and the forgot/reset-password views
(gone entirely - no local password exists to reset, per D15/D16/T10.4). Kept
the accessible error/ok banner pattern (role=alert / role=status) byte-for-
byte, since CLAUDE.md names this file as the reference other pages copy for
that pattern. login.js reads ?next= off its own URL (auth-guard.js's
goToLogin() already builds this, unchanged) and forwards it to
/api/auth/okta/login, and shows a plain-language message for ?error=disabled
/ ?error=cancelled, clearing the code from the address bar once shown. Sign-
out (auth-guard.js's wpLogout()) already redirected to login.html - untouched,
already satisfied "lands back on the app's own login page."
Uses a real <a href> rather than a JS-driven navigation, so it's a working
link even before login.js runs, and needs no keyboard/touch handling beyond
what a link gets for free (C1 accessibility).
Also fixed in passing (not a separate commit - this is what exposed it):
_safe_next_path() on the server and safeNext() in login.js enforce the exact
same rule (same-site path only, reject '//' and scheme URLs) so a crafted
?next= can't become an open redirect through a real Okta sign-in.
Verified: a fake-Okta-client round trip against the real app (SessionMiddleware
fix from the prior commit) confirms next= is honored end to end, a malicious
next= is rejected and falls back to /index.html, OAuthError redirects to
?error=cancelled, and a disabled account redirects to ?error=disabled. The
JS-side safeNext() was checked against the same cases directly in Node and
matches the server's validation exactly. login.js passes `node --check`;
login.html parses cleanly. Live 390px/1440px screenshots were NOT captured
this session - the environment's browser pane isn't signed in to view a
published preview of it, so that check needs to happen when this branch is
actually run and opened by a signed-in browser; the layout risk is low since
.card/.brand/.error/.ok/.foot are unchanged from the already-shipped file and
the only new CSS is one simple full-width block link.
wave-10.md T10.5 / D15 / D16
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>
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>
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>
- Logged-in users can change their own password from a "Password" link
in the top-right pill (dialog -> POST /api/auth/password, which requires
the current password).
- Login page gains a "Forgot password?" link explaining that resets are
admin-assisted (admins reset from the console). No SMTP, so no email
reset flow yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Gate the suite behind a self-contained login (no external IdP):
- User model with bcrypt-hashed passwords; admin/user roles
- /api/auth endpoints: login, logout, me, change-password, and
admin-only user management (list/create/delete/reset/enable)
- Stateless JWT session in an HttpOnly, SameSite=Lax, auto-Secure
cookie; middleware refuses every /api data route without a session
- login.html + auth-guard.js: login page and per-page guard with a
top-right "name / Admin / Sign out" pill
- Admin Console now gated on admin role (passphrase gate removed) with
a User administration card
- manage_users.py CLI to bootstrap the first admin
- Rebuilt help.js into a searchable, multi-topic help center
- Local-dev convenience: app serves html/ so the site + API share one
origin under uvicorn (inactive in the prod container)
- Docs/env: AUTH_SECRET_KEY, requirements (bcrypt, PyJWT), README
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>