495d87dd7228bd8efb7c03833e14888969e0446e
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| ab7bce9f5b |
T10.6 D13 - strip the password UI; "Forgot password?" goes to Okta
login.html/login.js: the two reset views are gone along with the reset-token handling, and the sign-in form now says which password to type - "your Windows password, the same one you use to sign in to your computer" - using the .hint class the page already had, so no new CSS and no new literal. "Forgot password?" is KEPT and points at https://primecontrols.okta.com/. An earlier draft of this task deleted the link and I proposed a plain "contact IT" sentence instead; Okta is the better answer, and with no app password and no break-glass it is the only recovery path that exists. Three details that would each have broken it: - The old click handler on #forgot-link called preventDefault() to swap views. Left in place it would have silently swallowed the navigation, so the link would look right and do nothing. There is now deliberately no handler, and login.js says why so nobody adds one back. - target="_blank" without rel="noopener noreferrer" hands the opened page a window.opener handle back to the login page. - Worth recording since it was checked rather than assumed: the CSP allows this. form-action 'self' governs form submission, not link navigation, and no navigate-to directive is set - so a plain <a href> off-origin is fine and the nginx config needs no change. login.js also handles 503 distinctly now. T10.2 made that mean "the directory is unreachable or misconfigured", which is our fault - showing "invalid password" would send people hunting for a password they no longer have while a deploy is broken. Also removed, because T10.3 deleted the endpoints behind them and leaving them would have produced visible 404s rather than dead-but-harmless markup: auth-guard.js the whole change-password dialog (POST /api/auth/password) wp-sidenav.js the "Password / Change your password" menu entry that opened it users.js the per-row "Reset password" action users.js the password field in the create-account form - NewUserIn no users.html longer accepts one, so the form was posting a rejected field The self-row placeholder button pointed at a top-bar Password link that no longer exists; it is now a plain "you" marker. Verified: node --check passes on all four touched JS files; the only password references left in html/ are the sign-in form and the SMTP config in admin.js, which is unrelated and stays. Logged BL-027 rather than acted on: the Okta URL is the first sign of an Okta tenant on this estate, which means an OIDC flow is available in principle and would remove the domain-lockout hazard that forced AUTH_MAX_ATTEMPTS to 2. D13 was decided and reaffirmed and T10.1-T10.4 are built, so swapping the mechanism mid-wave is the reordering CLAUDE.md forbids. Recording is not reopening. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 560f0cb3cc |
BL-024 - the last 21 native dialogs, onto the shared kit
S1 counted 79 native dialogs app-wide and its tasks removed 58; the audit found the rest on surfaces no S1 task named: admin.js (6), users.js (10), the launcher's inline script (5). All 21 now go through wp-dialog.js - the T7.9 kit extracted as a self-injecting shared component: markup and styles land on first use, styles are theme tokens only with its own wp-dlg-* class names (the consoles' existing .modal styles are untouched), 44px targets on coarse pointers, and the whole file is guarded so the creator's inline copy - which owns the same-id markup in its HTML - still wins on its own page. The kit's toast comes along (S10 role rules), since none of the three pages had one. Conversion follows the T7.9 precedent: confirms -> wpConfirmDialog with named ok-labels, the password prompt -> wpPromptDialog whose validate() finally enforces min-12 AT the input (it was label-text-only before, server-enforced), API failures with detail -> wpAlertDialog, small info/validation messages -> the announced toast. New probe console_dialogs_check (17): counts pinned at 0, kit guarded and loaded by all three pages, and the users console driven live with natives poisoned - reset a password end to end (short refused inline, good one accepted by the server and announced), cancel a delete and prove nothing died. Items: BL-024 (closed), S1 completed to zero app-wide, C1. Co-Authored-By: Claude Fable 5 <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>
|
|||
| 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>
|
|||
| 4ace2afb1c |
Move user administration to its own page; add Project Super User
User accounts lived in the Admin Console, which is admins-only. Project admins
need to create the accounts on their own jobs without an app admin on the phone,
so accounts move to a new User Directory page and a new role carries the right.
server/auth.py, server/app.py
New permissions role `project_super_user`, between admin and project_admin:
everything a project admin may do, plus user administration SCOPED to the
projects they hold the role on. Four limits make it safe to hand out, all
enforced server-side:
* Scope comes from projects, not the job title. It resolves per membership
(managed_project_ids), so an ordinary account can hold it on one job via
ProjectMember.role, and a super user demoted on one job administers
nobody there. No projects, no authority.
* Account-level changes (password, disable, rename, permissions, delete)
require EXCLUSIVE scope: refused when the target is also on a project the
caller does not administer, because those changes are global. The
directory renders such rows read-only with the reason.
* No admin or super-user targets, and neither role can be granted by a
super user -- that is the line that stops it becoming app-wide control.
* PUT .../projects rebuilds only the caller's own slice; memberships on
projects they do not administer are left untouched. A payload that simply
omits them must not cut someone off a job the caller cannot see.
Creating requires naming at least one of your own projects: an account with
none would be one the creator instantly cannot manage.
/api/auth/users is now scoped rather than admin-only, and carries a per-row
`manageable` verdict plus the reason. Non-managers get a contact card only --
a project user has no business reading colleagues' login history. New
/api/auth/user-scope tells the page what it may offer. Administrative
password resets are now audited; they were the one account change that left
no trace. Settings, feature flags and the auto-add rule stay admin-only.
While here: one definition of "is a user manager", derived from the managed
set. An account-role-only version disagreed with the scoped one and locked
per-project super users out of routes they were entitled to.
html/users.html, html/users.js
The directory: three renderings from one page -- admin (everything), super
user (controls per row, read-only where scope is shared), everyone else (a
read-only directory of the people on their own projects).
html/console.css, html/console-util.js
Extracted from admin.html/admin.js so both console pages share them. A
divergent jsq() is an XSS and a divergent role list offers permissions the
server refuses, so neither may exist twice.
html/wp-sidenav.{js,css}
Global nav drawer, role-gated, carrying ?project= across links. Mounted on
the field view (which had no way to anywhere) plus both console pages.
No migration: users.role is already String(20) and the new value fits.
Verified: 93 scope/gate tests, 29 live HTTP tests through the real dependency
stack, 33 static JS checks. Not verified in a browser -- no JS engine on this
machine -- so users.html and field.html want one manual load.
server/smoketest.py still fails with 401s. Pre-existing: it has no login code,
so auth_gate refuses it. Confirmed unchanged by stashing this work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|