Commit Graph

65 Commits

Author SHA1 Message Date
0a298b2f6e Add the Entra app-registration request for the SharePoint file picker
The written request for a `WP-Suite-File-Picker` app registration in the
prime-controls.com tenant: SPA platform, delegated Files.Read.All / Sites.Read.All
/ User.Read, admin consent, no secret and no app-only permissions. Keeping it in
the repo means the lowest-privilege configuration we asked IT for is on the record
next to the code that will use it, and whoever wires up the picker can see what
was actually granted rather than reconstructing it from memory.

Dated 2026-06-15, authored by Nick Siegfried. Committed on its own branch off main
because it has nothing to do with the user-directory work that was in flight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 15:41:03 -05:00
3cccdf1c4b Merge branch 'docs/deploy-runbook': project archiving, default members, admin console rebuild
Brings in the 2026-08-05 work plus the deploy runbook and KNOWN-ISSUES.md.
Carries migration a7c31f9e5b02 (additive, with server defaults). No overlap with
the entrypoint/backup-script changes already on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:46:27 -07:00
153fe97a31 Record the SOP discipline-name XSS as a known issue
It is a real hole and we are shipping without fixing it, so it needs to be written
down somewhere that outlives the conversation it came up in.

Discipline names are rendered into inline handlers in the WP creator escaped with
esc(), which maps ' to &#39;. That is right for text and wrong here: the browser
decodes entities in an attribute before the JS parser sees it, so the entity
becomes a bare quote and closes the handler's string literal. Escaping for a
handler argument has to go backslash, then quote, then HTML — esc() only does the
last part. Same bug, same ordering, as the two fixed on 2026-08-05 (jsq() in
admin.js, escHandlerArg() in work-package-suite-app.js); this one predates that
work and sits in a file it did not touch.

Left open rather than fixed because the suite is internal, behind a login, with
named employee accounts and no anonymous input path — the likely cost is a
discipline named "Owner's Equipment" silently breaking its own pill, not an attack.
The entry records the conditions that change that judgement (exposure outside the
corporate network, accounts for subcontractors or clients, self-registration), so
the rating cannot go quietly stale if the deployment story changes. Neither CSP nor
the CSRF gate mitigates it, and both are noted so nobody re-derives that hopefully.

Also records the archived-project rough edge from the same day: the server refuses
writes with 409, but the two big apps still present Save and Issue buttons, so the
failure is safe but late. data-wp-archived is already on the document element for
whoever closes it.

Each entry says what closing it takes, and entries get deleted in the commit that
fixes them — otherwise this file becomes a museum instead of a queue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:56:27 -07:00
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>
2026-08-05 13:48:43 -07:00
7bb1f66588 Merge pull request 'Add entrypoint fallback for missing bind-mounted scripts' (#2) from scripts/entrypoint into main
Reviewed-on: #2
2026-08-05 01:56:49 +00:00
26f4a9242a Entrypoint static variable removed to allow validation of scripting files before startup 2026-08-04 18:51:08 -07:00
6c1dc7d45f Add entrypoint fallback for missing bind-mounted scripts
- scripts/entrypoint.sh (new): prefers the live bind-mounted backup-cron.sh, but falls back to a copy baked into the image at build time if the mount is missing. If neither exists, it stays up and idle (instead of crash-looping) so the container remains reachable via console/exec for diagnosis.
- scripts/backup-cron.sh (updated): resolves db-backup.sh the same live-or-fallback way, re-checked on every loop iteration, so if the bind mount comes back healthy later, this container picks up the live scripts on its next backup run with no restart needed.
- scripts/backup.Dockerfile (updated): bakes all three scripts into the image under /app/scripts-default/ as the fallback, and sets the new wrapper as ENTRYPOINT.
2026-08-04 18:44:34 -07:00
e5977758c0 Hand-off runbook for the 2026-08-04 deploy
A step-by-step deploy procedure for someone who administers the Docker host
but does not know this app. Two things about this deploy need spelling out for
them, and neither is obvious from DEPLOYMENT.md:

- nginx's config and all of html/ are baked into the image, so the stack has to
  be re-pulled and re-built. A restart deploys nothing and looks like a success.
- the pending users.role rewrite (b41c7ae9) is one-way as far as the app is
  concerned: rolling the API image back after it commits breaks logins, because
  the old code doesn't recognise 'project_user'. So the runbook records
  `alembic current` and both image IDs up front, and splits rollback by symptom
  — an nginx-only failure is a safe code-only rollback they can do alone, a
  failed migration is escalate-don't-improvise.

Backups go through the existing sidecar rather than an ad-hoc pg_dump: it works
from Portainer's console without SSH, writes an encrypted timestamped dump to
backups/ on the host, and prints a success line worth checking. Commands use
`docker exec <name>` throughout, since `docker compose` from an SSH session
can't find a Portainer-managed stack's compose project.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:10:15 -07:00
a9b22f2add NGINX: set Cache-Control via a map, not a nested location, so security headers survive
The Cache-Control rule I added in the previous commit used a nested
`location ~* \.(html|css|js|webmanifest)$`. nginx does NOT inherit add_header into a
block that declares its own add_header, so every HTML, CSS and JS response would have
been served WITHOUT the CSP, HSTS, X-Frame-Options, Referrer-Policy and nosniff headers
from the Phase S hardening — the headers dropped for exactly the files that matter most,
and silently, since the pages would still work.

Now computed by `map $uri $wp_cache_control` at http level and applied with one
server-level add_header alongside the security headers, so nothing is scoped away. An
empty value makes nginx omit the header entirely, so images and fonts stay cacheable.
Applied to both the Docker config (nginx/conf.d/wp-suite.conf) and the bare-metal one
(nginx-wp-suite.conf), which carries the same header set.

Caught while checking whether the stack was safe to redeploy. Not verified with
`nginx -t` — this machine has neither nginx nor docker — so DEPLOYMENT.md now records
the rule and the one-line curl that confirms both headers are present after a deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:37:18 -07:00
e3527a6e1d Act on the fragility audit: boot-order crash, real cache correctness, deep links
A 55-agent audit of the last few commits confirmed 32 findings. The high and medium
ones are fixed here; the ranked leftovers are listed at the end.

Boot-order crash (my regression, wave 2)
- wp-format.js loaded AFTER wp-creation-app.js on every page, but the creator boots
  synchronously at parse time and its comment renderer calls wpFormatDateTime(). With
  any review comment present that threw a ReferenceError and aborted the rest of boot.
  The formatter now parses before the app scripts on all five pages. Verified with a
  comment seeded: the date renders and boot completes.

The network-first fix didn't actually work
- `fetch(req)` inherits the request's default cache mode, so it consults the browser
  HTTP cache — the previous commit's "network-first" still allowed a page to run
  against a stale sibling. Code is now fetched with cache:'no-cache' and precached
  with cache:'reload'.
- Nothing pinned freshness on the wire either: no Cache-Control anywhere, so browsers
  applied heuristic caching (~10% of a file's age) and each file expired at a
  different moment. NGINX and the dev server now send no-cache for html/css/js/
  webmanifest; images stay cacheable. Verified on the wire.
- Non-ok responses were returned verbatim, so a 502 broke pages the cache could have
  served; they now fall back to the cache. Cache keys drop the query string, which
  fixes both the offline miss on every in-app link (?project=…&tab=…) and unbounded
  cache growth. respondWith can no longer resolve to undefined. Cache bumped to v5.

Embedded creator
- Dropped the &t=Date.now() cache-buster and made the frame's identity the PROJECT.
  The view and which package to open are now applied by calling into the loaded
  document, so switching tabs no longer reloads it — that reload discarded unsaved form
  edits, made the creator unreachable offline, and stored a fresh copy per click.
- ?view=dashboard was re-read on every tab switch, so after one deep link the
  "Work Package Creation" tab kept opening the Dashboard for the rest of the session.
  Deep-link params are consumed once now.
- ?wp=<id> — which the global search has been emitting since wave 2 — was read by
  nothing, so picking a work package in search opened a blank one. The creator now
  exposes openWpById() and the shell applies it after a new 'wp-creator-ready' event,
  because the frame's load fires before pullProject() resolves.
- Math.max(320,…) could make the frame taller than the space available while page
  scrolling was disabled, pushing content off a window that couldn't scroll. Full-bleed
  is now only used when at least 460px remains, and the SOP-incomplete gate never runs
  inside it. A ResizeObserver re-measures when wp-chrome.js grows the app bar.

Contract drift
- .field-hint and .user-pick are used on the SOP suite page but their only rules lived
  in wp-creation-styles.css, which that page doesn't link — the CM hint and the
  sign-off pickers had no styling at all. Rules added to the suite's stylesheet.
- The creator's critical floor now also hides modal overlays (a stale stylesheet
  rendered their contents inline in the form) and gives the jump bar a sane sticky top.
- login.js dereferenced ids unguarded where the old version guarded, so a cached older
  login.html would break sign-in itself. Guarded.
- The "Language & time" menu item was added only if wp-format.js had already parsed;
  the check now happens at click time.

Verified: 157 API checks across five suites on a clean database, plus 22 driven UI
checks — boot-with-comment, tab switching with a no-reload probe, short-viewport
fallback, and the search deep link landing on the right package.

Not done, ranked: ~50 dead CSS rules across three stylesheets; dead .team-pick and
.constraint-option contracts; wp-chrome.js's documented '.header' mount branch is
unreachable because the creator loads neither wp-chrome.js nor its CSS; the squeeze
half of the embed layout (.content-area.embed-full) is still CSS-only, which degrades
to the old narrow column rather than breaking; fingerprinted asset URLs would make a
mismatched pair unrepresentable rather than merely unlikely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:07:20 -07:00
917a728399 Rebuild the work-package side panel in the style of MS Planner
The auto-hiding drawer was the wrong model — a list you navigate by shouldn't appear
and disappear under the pointer, and its vertical text tab read as a stray artifact.
Replaced with a persistent side panel following the Planner reference:

- Collapse toggle at the top (the panel glyph, arrow flips), remembered across visits.
  Collapsed leaves a 56px icon rail where the coloured package badges are still
  clickable, rather than hiding the list entirely.
- One primary action: "+ New work package" with a split caret for Duplicate, Split by
  discipline and Export all.
- Icon nav with counts: My packages (owned by you), All packages, Needs attention
  (on hold or not release-ready), Dashboard. These filter the list below.
- Packages as rows with a colour-coded initial badge, number, subject and readiness
  state, still grouped by status, with a left accent bar on the current package.
  The badge colour is hashed from the WP number, so a package keeps its swatch
  instead of shuffling when another is added or deleted.
- The panel sits IN the layout: the form and the full-width chrome shift beside it
  rather than being overlaid.

Also, the reason it appeared as loose unstyled widgets in the middle of the form: the
panel's markup and its stylesheet are cached independently, so a browser can run new
markup against old CSS. Its essential layout (fixed position, width, the row/badge
flex, the collapsed rules) is now injected by wp-creation-app.js as a floor, inserted
first in <head> so the stylesheet still wins on everything it defines. Same lesson as
the iframe: a component whose CSS-missing state is "broken" rather than "plain" must
carry its own critical layout.

Verified with 25 driven checks in headless Chrome: persistence, the four nav links,
badge colours and text, view filtering, collapse/expand, the split menu, row selection
and highlighting — and, with wp-creation-styles.css removed from the page entirely, the
panel is still a fixed 288px side panel with the form shifted beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 21:47:40 -07:00
40bd19b6cf Stop the embedded creator collapsing to a 300x150 box; serve code network-first
The Creator rendered as a tiny double-scrolling square in the WP tab. My fault, and
the mechanism matters more than the symptom:

I had moved the iframe's sizing (width:100%, border:0, min-height) out of its inline
style attribute and into work-package-suite-styles.css. The service worker cached the
HTML and the stylesheet as INDEPENDENT entries, cache-first — so a browser could hold
the new HTML together with the old CSS. With the inline sizing gone and the new rule
absent, the iframe fell back to the HTML default 300x150 box and the whole tool
collapsed. Moving self-contained markup into a separately-cached file created that
window; nothing about the layout itself was wrong.

Three layers so it cannot recur:
- The iframe's width/border/min-height are inline again, on purpose, with a comment
  saying why. An iframe with no intrinsic size has a catastrophic failure mode, so its
  sizing must not depend on another file being in step.
- applyEmbedLayout() now sets the fill height and width as INLINE styles via
  sizeWPFrame(). Inline beats any stylesheet, including a stale cached one, so the
  class is a refinement rather than a requirement.
- sw.js: HTML/CSS/JS are now fetched NETWORK-FIRST with the cache as offline fallback;
  images/icons/manifest stay stale-while-revalidate. These files reference each other,
  so a page must never run against a stale sibling — this same staleness had already
  masked two other fixes during development. Cache bumped to v4.

Verified: at 2560x1440 the tool spans the window with a single scrollbar; with
work-package-suite-styles.css removed entirely (strictly worse than stale) the frame
still measures 1469x662 instead of 300x150, and re-running the layout pass keeps it
there; 12 checks across sop -> wp -> dashboard -> sop confirm body.embed-full, the
content-area class, the fill class and the inline height are all cleared on the way
out, so the wizard never ends up unscrollable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 21:27:53 -07:00
6c3098922f Field view: a package waiting on a predecessor is not "Ready"
The field list judged readiness from open constraints alone, so a package whose
predecessor isn't Closed showed a green Ready pill even though the server would
refuse to issue it. It now shows "waits on N", matching the form, the dashboard and
the navigator drawer. A deleted predecessor still doesn't block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 17:26:27 -07:00
fcba74b584 Fix the WP navigator and the squeezed embedded layout; add per-project permissions
Layout — the reported "skinny scrolling windows"
- .content-area capped the whole suite at 1000px, so on a 1920 screen the embedded
  Work Package Creator ran in a ~930px column with its own scrollbar inside the
  page's. The wizard now caps at 1700px and the Creator/Dashboard tab goes
  full-bleed: the iframe fills the window below the app chrome and owns the only
  scrollbar. Needed `flex: none` on the content area — as a `flex: 1` item its
  flex-basis overrode `height`, leaving the used height indefinite so the child's
  `height: 100%` collapsed the iframe to its 150px default.
- The SOP wizard's fields were one per row; they now flow into ~340px columns.

Navigator — now an auto-hiding drawer
- It was a fixed 262px column that stole width from the form AND was hidden below
  1100px, so embedded (the normal path) it never appeared at all — that's the
  "broken side menu". It's now an overlay drawer behind a slim always-visible edge
  handle: hover or tap to open, move away / Escape / pick a package to close, or pin
  it to keep it open (pinned shifts the form and the page chrome across, and is
  remembered). A gutter keeps the handle off the section-nav chips.

Bugs found while checking the site over
- collectStepData() still read the SOP team fields as text inputs, but wave 1 made
  them account pickers — so it wrote a user ID into state.team.pm where the display
  NAME belongs, and the SOP would print `user_ab12…` as the PM. Now synced properly
  from the pickers.
- loadSampleData() set .value on those selects with fictional names; setting an
  unmatched value on a <select> silently does nothing, so the sample lost its team.
  It now stores them as names without an account, which the picker shows as
  "(no account)".
- My earlier CSS block replacement had deleted the SOP-chip, people-picker and
  critical-tag styles. Restored.

Same picker everywhere the SOP names someone
- Sign-off roles (step 3, required and optional) are account pickers now, storing
  userId alongside the name, so a signature belongs to an account that can be
  notified. Titles stay free text.

Per-project permissions (asked for: "change project permissions for individual users")
- project_members.role overrides the account's role on that project, so a PM on one
  job can be a Project User on another. Empty = inherit; app admin is admin
  everywhere. effective_role() feeds require_project_admin, so WP delete, completed-
  SOP edits and project delete are all judged per project.
- Project access is now its own column in the admin console (it was buried among the
  action buttons, which is why it couldn't be found), showing the project count per
  account; the dialog sets access plus the role on each project.
- The members endpoint reports each person's effective role on that project.

Verified: 157 API checks across five suites on clean databases (44 permissions +
22 password reset + 34 search/localization + 39 gates/notifications + 18 new
per-project permission checks), 16 drawer-behaviour + 4 pinned-mode UI checks driven
in headless Chrome, and probes confirming the team/sign-off pickers populate and no
longer corrupt state.team on step navigation. Screenshots reviewed at 1920x1080.

Service-worker cache bumped to v3 so browsers pick up the new shell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 17:22:19 -07:00
b38348e6ae Wave 3: predecessor references with a release gate, and critical-constraint reopen alerts
Predecessors are real references now
- data.predecessors holds work-package ids, replacing a free-text SOP phase label
  that couldn't express "WP04 waits on WP02" and gated nothing. The SOP phase
  survives beside it as the descriptive "Sequence phase" field.
- readiness() has two gates: constraints clear AND every predecessor Closed. The
  banner, sticky bar, left rail, dashboard Gates column and the ready counters all
  reflect the second one.
- Enforced server-side by enforce_release_gates() on every path that sets a status
  — the plain upsert included, since that's how the browser and the offline outbox
  save. /issue and /status would otherwise have been ways around it.
- Cycles are refused directly and through a chain, with a message naming the
  package that already waits on this one. The Creator's picker also hides itself
  and its own descendants, so a cycle is hard to build in the first place.
- A deleted predecessor does not block: it would freeze everything downstream of a
  package someone removed.
- The gate is refusable, on purpose. Planners release ahead of upstream close-out,
  so an explicit reason (data.gateOverride) allows it, gets a gate_overridden audit
  event naming what was skipped, and prints on the package. A blank reason is not
  an override, and changing the predecessor set clears it. The dashboard won't
  release a blocked package at all — it points at the form where the reason is
  captured.

Critical constraints reopened after release
- Reopening a SOP-critical constraint on a released package emails the owner, PM,
  CM and the package's distribution list (minus whoever did it) and writes a
  constraint_reopened audit event.
- Detected by diffing the incoming constraints against the stored ones inside the
  normal upsert rather than via a new endpoint: the sync outbox only replays
  POST /api/wps, so a dedicated route would be lost offline. It fires only on a
  real cleared→open transition, so re-saving an already-open constraint doesn't
  re-announce, and never before release or for a non-critical constraint.
- Bodies carry the constraint name, WP number and a link — never package contents.

Verified: 139 API checks on one fresh database (44 permissions + 22 password reset
+ 34 search/localization + 39 gates/notifications), including every bypass path,
cycle shapes, the deleted-predecessor case, blank-reason overrides, and the four
recipients confirmed both in the outbox and on the wire against a local SMTP sink.
27 driven UI checks against the real Creator page in headless Chrome covering the
picker, the override prompt (accept and cancel), override invalidation, the cycle
exclusions and the dashboard refusal. Screenshots reviewed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 16:15:36 -07:00
61d1cf4bff Wave 2: form cleanups from the site comments, plus localization, project switcher and global search
Site comments (8/3)
- BIM card: LOD removed, IFF # added next to the coordination status, and required
  once that status is "Signed off (IFF)" — an unnumbered sign-off isn't traceable.
  A LOD already stored on a package is preserved and shown as legacy, not blanked.
- The blue "from SOP types" subtext under a field is now a SOP chip on the label
  with the detail in a tooltip. The chip stays visible rather than hover-only:
  field tablets have no hover, and "this came from the SOP" is the part that
  matters. The hint elements stay in the DOM (hidden) so the code writing to them
  keeps working; an observer mirrors their text into the tooltip.
- Specification Section is no longer typed per package. Each WP type carries a
  spec section on the SOP; the field is read-only in the Creator and follows the
  type, with the SOP's spec folder linked underneath. This reads both spec
  comments as one intent — stop typing it, derive it.
- Assignees and Distribution are multi-selects over the SOP project team, showing
  each person's job function, with the CM pre-added to Distribution (removable per
  package) and a free-text option for people with no account. The stored display
  strings are unchanged so print/export/dashboard keep working; account ids ride
  alongside for the notification work in wave 3.

Localization + time
- Per-user locale/timezone (Language & time in the user menu), an app-wide default
  in the admin console, then the browser. Timezones are validated against the
  server's zoneinfo and the picker is fed from it. Calendar dates are formatted
  from their parts so a due date never reads a day early in another zone.
- Every displayed timestamp now goes through the shared helpers.

Top-bar chrome
- Project switcher beside the logo and a centered global search, injected into
  either generation of top bar; skipped in an iframe so the embedded Creator
  doesn't get a second one. Ctrl/Cmd-K focuses search.
- GET /api/search covers work packages, projects and SOPs, scoped to the caller's
  projects, hiding archived packages, with LIKE wildcards escaped.

Fixed along the way: showForm() cleared every card's inline display, which undid
applyKind() — so the Package Type and BIM cards reappeared on an install-only
project. Split out applyKindVisibility() and re-apply it there.

Verified: 100 API checks on a fresh database (44 permissions + 22 password reset +
34 search/localization), 24 driven UI checks against the real Creator page in
headless Chrome (SOP chips, both people pickers, spec auto-fill, critical tags,
BIM suppression), and the chrome harness on both bar styles. Screenshots reviewed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:17:51 -07:00
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>
2026-08-03 14:48:59 -07:00
1d004cab75 Widen the IWP screen and add a left work-package navigator
The Work Package form was capped at a 1000px column, which wasted most of a
desktop screen, and the only way to reach another package was to scroll to the
Saved table at the bottom.

- Put the form in a wide two-column shell (max 1760px); ctx-bar, mode-wrap and
  the release banner widened to match.
- Above 1200px the two-up field grids flow to 3-4 columns instead of stretching
  two fields across the whole card. Narrow layouts are unchanged.
- New sticky left rail listing every saved package, grouped by status in field
  order, with WP number, subject, readiness dot and type. Click to open it in
  the form; the package being edited is highlighted. Filter box, + New and
  Dashboard shortcuts, collapsible (state persisted), hidden under 1100px where
  the Saved table still covers navigation.
- The rail re-renders from renderSavedList(), so saves, deletes, splits,
  archive/restore and the project pull all keep it current.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:59:34 -07:00
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>
2026-07-15 17:51:15 -07:00
dd37f1f551 Keep the 2D sheets / spool-drawings step out of install-only sequences
That deliverable is a BIM/EWP output (and on install-only jobs Prime
often doesn't own it), so it no longer appears in the default IWP
sequence. It now lives in the BIM sequence as the hand-off step, so it
only shows on BIM-enabled projects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:58:01 -04:00
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 e5102446 on shared-data) on top of the BIM / per-package work.
localStorage becomes a per-browser cache; the server is authoritative.

- project-data.js: pullProject() hydrates the apps' existing localStorage
  keys from the API on load; pushSOP()/pushWP()/removeWP() write through
  on save/delete. WPs store the whole flat object in `data`, so BIM
  fields, kind, and projectLinks round-trip intact.
- index.html: home pulls the project before showing SOP status; feedback
  loads from /api/comments (server-authoritative, local fallback).
- work-package-suite-app.js: pull-then-restore on boot; completeSOP
  pushes the SOP to the server.
- wp-creation-app.js: save/duplicate/issue/setStatus push; delete/clear
  remove; boot pulls from the server first, then boots off the cache.
- server/app.py: /api/sops and /api/wps take full=true to return the
  data JSON for one-request hydration (list stays lean by default).

Co-Authored-By: C-West8 <125926137+C-West8@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:58:33 -07:00
ca4176ac00 Sequence: BIM steps first, and default flow matches the field spec
- enableBIM() prepends the BIM steps (BIM precedes construction) instead
  of appending them.
- Default construction sequence updated to the agreed flow (2D sheets /
  spool drawings -> conduit -> tray -> QC hold -> wire pull -> device ->
  termination -> QC hold -> commissioning -> as-built), with QC-hold gates.
- Sample project now enables BIM/VDC so it demonstrates the full
  BIM -> construction sequence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:49:11 -07:00
0e698438a2 BIM as a per-package kind; one project flows BIM -> construction
Replace the whole-project BIM 'mode' with an opt-in capability + a
per-package kind, so a single project can produce both model and
install packages (and install-only projects are unaffected).

SOP tool:
- Step 4 "Include BIM / VDC work packages" checkbox (state.bimEnabled).
  Enabling adds BIM package types + BIM release gates (flagged bim) plus
  BIM roles/sources/process steps alongside the construction defaults;
  disabling strips the bim-flagged items.
- Generated SOP carries bimEnabled and a per-type / per-constraint bim flag.
- Required sign-off role titles stay editable (no longer force-renamed).

Work Package Creator:
- Shows a Package Type selector (Install IWP / BIM EWP) only when the
  SOP has bimEnabled; kind is saved per package and labeled in the output.
- WP types and release gates are filtered by kind (BIM types+gates for
  EWP, install types+gates for IWP).
- EWP reveals the BIM Details card and hides controls.dev Assets /
  Materials / Kitting-MIMO; IWP shows those plus the "Enabled by - BIM
  package" traceability link.

Supersedes the earlier whole-project BIM mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:07:08 -07:00
566ace9969 Add BIM/VDC work packages, platform links, and AWP traceability
SOP tool:
- "Load BIM / VDC template" (Step 4): one click sets BIM deliverable
  types, install-phase disciplines, BIM release-gate constraints (with
  EN06 citations), the BIM process sequence, reference sources, BIM
  sign-off roles, and MWP##-[Area]-[PHASE] numbering. Tags the SOP mode
  as 'bim'.
- Step 3 required sign-off role titles are now editable (default
  Superintendent/Foreman); the BIM template sets them to BIM Coordinator
  and Construction Lead (CRS).
- Step 7: capture a project-homepage link for the chosen tracking and
  commissioning platforms.

Work Package Creator:
- BIM mode (SOP.mode==='bim'): hides controls.dev Assets, Material List,
  and Kitting/MIMO; shows a BIM Details card (LOD, model area, clash /
  coordination status, linked scan).
- Project tracking/commissioning links are copied onto every WP and
  shown in the WP output.
- "Enabled by - BIM package(s)" field links a field IWP back to the BIM
  package that enabled it (EWP -> IWP traceability).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:54:57 -07:00
eefa76e460 Add self-service password change + forgot-password guidance
- 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>
2026-06-30 13:20:49 -07:00
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>
2026-06-29 16:38:56 -07:00
bdb798efdd Add Portainer deploy guide for the login portal
Standalone instructions for the Portainer admin: set AUTH_SECRET_KEY,
rebuild + redeploy the stack, and bootstrap the first admin account.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:52:27 -07:00
151ccea0ac Merge remote main (nginx conf fix) into login portal 2026-06-25 17:00:13 -05:00
1f8c23c9bb Merge feat/secure-login-portal: secure login portal 2026-06-25 16:59:34 -05:00
20afb0e565 Add secure username/password login portal
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>
2026-06-25 16:55:54 -05:00
deaf13c724 fix server in wp-suite.conf so proxy works. 2026-06-17 13:07:49 -05:00
a02f7ec511 Prevent table-creation race: gunicorn --preload
With 2 workers, both ran Base.metadata.create_all() at import on an empty DB,
racing on CREATE TABLE (duplicate pg_type for "projects"). --preload imports
the app once in the master before forking, so tables are created a single time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:03:38 -07:00
c010bc22a0 Update home footer to BTG / Pilot Use Only
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:40 -07:00
66da5b708a Fix API DB connection: build URL from POSTGRES_* (auto-encode password)
The api crash-looped because DATABASE_URL had an un-encoded special-char
password (@/!), so SQLAlchemy parsed part of the password as the host
("...@db" → name resolution failure).

db.py now prefers building the connection from POSTGRES_USER/PASSWORD/DB via
SQLAlchemy URL.create(), which encodes the password automatically — any
password works with no manual escaping. DATABASE_URL remains an optional
override (still must be hand-encoded if used). docker-compose now passes the
POSTGRES_* vars to the api container; DEPLOYMENT.md updated (incl. a Portainer
env-vars note).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:17 -07:00
1e31aa535e Merge feat/admin-console into main
Adds /admin.html — a passphrase-gated, in-site admin console for diagnostics
and testing: API connectivity check, DB snapshot, browser smoke test, and
demo-data seed/clean. Gate is SHA-256 obfuscation only (default passphrase
"prime-admin"); restrict at the network/proxy for real protection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:09:17 -07:00
ca8c36a889 Add in-site Admin Console (gated diagnostics + tests)
html/admin.html + admin.js — a passphrase-gated console served at /admin.html:
- API connectivity check (clearly flags the /api/ 404 if the proxy isn't routing).
- Database snapshot (project/SOP/WP/comment counts via the API).
- End-to-end smoke test in the browser (mirrors smoketest.py: issue gate,
  status, metrics, comments) with self-cleanup.
- Demo data: seed a DEMO project + clean DEMO-/SMOKE- projects.

Gate is SHA-256-based (default passphrase "prime-admin"; documented how to
change) — obfuscation only, not real auth; restrict at the network/proxy for
real protection. Not linked from the main nav.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:06:04 -07:00
39230adf07 Merge fix/round1-feedback into main
Round-1 test feedback + verification tooling:
- Constraints: fix custom constraints never appearing; free-text add + remove.
- Sources: column headers; preset (hard-coded) data types, Add Source for extras.
- Issuance strategy: tooltip + worked examples.
- Remove "comment submitted" popups; keep commenter name.
- WP: offer to issue when the last constraint clears; collapsible sections.
- server/smoketest.py (end-to-end API/SQL check) and server/seed_demo.py
  (loadable demo project); DEPLOYMENT.md documents both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:50:04 -07:00
2bdb65e580 Add seed_demo.py loadable demo project + document both test scripts
server/seed_demo.py seeds a realistic DEMO project (complete SOP + a spread of
Work Packages: issued, gated, multi-discipline master with split instances,
overdue, over-threshold draft) via the API. --clean removes it. DEPLOYMENT.md
documents both smoketest.py and seed_demo.py, including the localStorage caveat
(seeded project shows in the UI picker; seeded SOP/WPs are SQL-only until Phase 2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:28:11 -07:00
e3ef3b0023 Round-1 test feedback + smoke-test script
- API smoke test (server/smoketest.py): stdlib end-to-end check of health,
  projects, SOPs, WPs, the AWP issue gate (409 → 200), status, metrics,
  comments, and cascade delete. Referenced from DEPLOYMENT.md.

SOP config:
- Constraints: fix custom constraints never appearing — renderStandardConstraints
  no longer clobbers state.constraints; customs render in their own list with
  remove buttons; modal gains a free-text "Add" field.
- Sources: add column headers (Data Type / Location-Platform / URL / Notes);
  preset data types are now fixed labels, "Add Source" creates an editable
  custom row.
- Issuance strategy: add a tooltip + worked examples for each option.
- Remove the "Comment submitted" acknowledgement popup (home + suite); keep the
  commenter name between comments.

WP creator:
- Clearing the last open constraint now offers to mark the package Issued and
  scrolls to the status control.
- Form sections are collapsible (click a section heading to fold it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:04:46 -07:00
c64b5c8b49 Merge feat/gui-polish into main
Integrates the full feature set built on top of the Docker/Postgres deployment:
- Discipline strategy, per-discipline scope/status, Split by Discipline (WP01A/B/C),
  material-by-discipline.
- WP Dashboard (metrics, gating, filters) + backend issue/status/metrics endpoints.
- Multi-project support: projects entity + CRUD, project picker home page,
  per-project data isolation.
- WP sizing presets, Duplicate WP, custom WP types, menu/UX cleanup.
- GUI polish: Help section, tooltips, sticky save bar + section nav, status pills.
- Rewritten DEPLOYMENT.md for the SQL-backed Docker stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:36:39 -07:00
a32c275f76 Rewrite DEPLOYMENT.md for the SQL-backed Docker deployment
Replaces the stale non-Docker/systemd guide with an admin-facing, start-to-
finish guide for the actual stack (nginx serving html/, FastAPI api, Postgres
db). Covers prerequisites (external proxy network), the root .env credentials,
reverse-proxy wiring, bring-up, and verification. Adds the current data model
(projects + project_id/parent_id/issued_at columns), the full endpoint list, a
"what's stored in SQL today vs Phase 2" table, backups, and the schema-migration
caveat (create_all adds tables, not columns). Points to server/README.md for the
deep container reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:27:38 -07:00
e5f77846ad GUI polish: Help section, tooltips, sticky save bar + section nav, dashboard filters
- Help: shared help.js injects a Help modal (workflow + key concepts) and the
  .help-tip tooltip component. " Help" added to the suite header and home nav.
- Tooltips: ⓘ hover hints on the trickiest fields (WP number auto-build,
  disciplines, scope/split, constraints, materials-by-discipline, discipline
  strategy, split threshold).
- WP creator: sticky section-nav jump chips at the top and an always-visible
  sticky save bar (Save Draft / Save & View) showing live release readiness.
- Dashboard: metric cards (Release-ready / On hold / Overdue / Total) and the
  status chips are now clickable filters for the board.
- Consistent colored status pills in the dashboard board and the saved list.

Theme unification (home Carbon vs tools palette) intentionally left for a
separate pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:37:01 -07:00
561d4f2408 Menu/UX cleanup: cost codes, Duplicate WP, condensed toolbars, Dashboard tab
- Cost codes: drop everything after 4990 (material/quality/admin codes removed).
- WP creator: add "Duplicate" (asks how many copies; each a clean Draft with
  unique number/subject, approvals & closeout cleared). When embedded the WP
  toolbar now shows only New + Duplicate.
- Condense duplicated toolbars: the embedded creator's Usage Data, Load Example/
  Sample SOP, Comments, View SOP and Dashboard buttons are hidden; the suite's
  top-right "Usage Logs" and "Load Sample" are the single instances.
- Load Sample is context-aware: SOP tab loads the sample SOP, WP/Dashboard tab
  loads the example Work Package in the creator.
- Dashboard moved to a nav-tab next to SOP Configuration / Work Package Creation.
- Remove "Prime Controls" branding from the embedded WP menu.
- Remove "Bill Clarida" from the example WP distribution and the step-comments
  name placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:36:14 -07:00
a18ae487f6 Fix SOP→home flow, stop sample-data fallback for real projects, add custom WP types
- SOP Complete now returns to the project home page after the confirmation
  popup (instead of staying on the SOP tab), and stamps projectId onto the SOP.
- WP creator no longer substitutes the Micron SAMPLE_SOP when a project is
  active but its SOP isn't found — it shows a "complete the SOP first" empty
  state instead. Sample is only used for a standalone (no-project) preview.
  This was the source of "loaded with sample data I didn't select."
- SOP WP Types step gains "+ Add Custom Type" (editable name + remove);
  blank-named custom types are dropped from the generated SOP. Custom types
  round-trip via the saved state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:29:14 -07:00
68c1c803d6 WP size: preset dropdown that sets the split threshold + show band in creator
Replaces the free-text "Typical WP Size" with a preset dropdown (Small /
Standard / Large / Custom). Choosing a preset auto-fills the max-hours split
threshold (still editable). The chosen band is now surfaced next to Est. Hrs
in the creator's size hint (not just the View-SOP modal), alongside the
threshold/over-threshold warning. Sample SOP aligned to Standard (80 hrs).
repopulateForm preserves a non-preset saved value as a dropdown option.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:17:18 -07:00
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>
savepoint-multiproject-done
2026-06-15 15:46:59 -07:00
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>
2026-06-15 15:39:45 -07:00
4d111d608d Merge branch 'main' into feat/wp-discipline-split-dashboard savepoint-multiproject-start 2026-06-15 15:28:35 -07:00
0baf4befc0 Tag material list by discipline for multi-discipline WPs
When a package carries 2+ disciplines, the BOM gains a per-line Discipline
column (hidden otherwise). Split by Discipline routes each instance only the
material lines tagged to it; untagged lines stay on the kept master, and the
split summary flags how many were untagged. Rendered doc shows the column
when any line is tagged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
savepoint-pre-multiproject
2026-06-15 15:20:37 -07:00
d1f0901897 Add discipline strategy, WP sizing, Split-by-Discipline & dashboard
SOP config (Governance step) now sets project discipline policy:
- disciplines list, discipline strategy (single/multi/planner-choice),
  letter instance-suffix style, and a max-hours split threshold.

WP Creator becomes discipline-aware:
- discipline picker; selecting 2+ turns the flat scope into per-discipline
  scope sections, each with its own status (rolls up to least-advanced).
- "Split by Discipline" turns a multi-discipline WP into WP01A/B/C instances
  linked to a kept master (instanceOf/parentNumber/split/children).
- est-hours warning against the SOP split threshold.

New WP Dashboard (header button, home card, ?view=dashboard deep-link):
metrics, status/discipline breakdowns, a gating panel, and a filterable
board with view/edit/issue. Reads localStorage via an API-ready WPData
adapter; masters excluded from counts.

Backend: POST /api/wps/{id}/issue (enforces the constraint gate),
POST /api/wps/{id}/status, GET /api/wps/metrics; work_packages gains
parent_id + issued_at.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 13:29:41 -07:00