Skip to content

Release v1.3.2: BUILD REPORT, Onshape settings UI, hierarchical seed templates#50

Closed
abbyfluoroethane wants to merge 38 commits into
masterfrom
devel
Closed

Release v1.3.2: BUILD REPORT, Onshape settings UI, hierarchical seed templates#50
abbyfluoroethane wants to merge 38 commits into
masterfrom
devel

Conversation

@abbyfluoroethane

Copy link
Copy Markdown
Collaborator

Summary

Two feature commits plus the v1.3.2 version bump.

BUILD REPORT, expanded MCP procedure tools, hierarchical seed templates (d73b421)

  • New printable `/executions/{id}/report` page with closeout-photo support — new attachment kind `closeout`, `PATCH /api/attachments/{id}`, template + CSS, smoke test.
  • MCP server: full procedure CRUD (get/update/delete procedure & steps, reorder, dependencies, kit/output management, step kit, publish_version, list_versions, clone_procedure) plus create_risk. Adds `mcp>=1.0` dep.
  • `opal migrate` now honors `--project` / `--database` so migrations target the right project DB.
  • Seed: rewrite all 6 procedures into operation/step hierarchy (52 ops, 194 sub-steps with requires_signoff / required_data_schema on measurement & verification steps); add default users (admin id=1 + two non-admins); embed parent_step_id / kit_items / output_items in version snapshots so executions render the tree.

Onshape settings UI (07b0194)

Admin-only `/settings/onshape/configure` form so credentials can be entered, edited, and tested without touching `.env`.

  • New `AppSetting` model + migration for runtime-mutable key/value config that survives restarts and overrides env values.
  • `apply_db_overlay()` rebuilds runtime Settings from `app_setting` rows at server lifespan startup and after each save.
  • `OnshapeClient.get_session_info()` powers the TEST CONNECTION button; bad credentials surface as a friendly "Authentication failed" banner instead of a raw JSON decode error.
  • Settings index always shows the ONSHAPE INTEGRATION panel: CONFIGURE button when disabled, EDIT alongside PULL/PUSH when enabled.
  • Form follows the standard OPAL panel + `form_actions` style; reuses `ok.status` and `var(--accent-*)` for messaging. Drops broken `.alert` classes in `onshape_documents.html` for the standard inline-color flash style.

Test plan

  • `ruff format` + `ruff check src/` clean
  • `pytest tests/unit/` — 308 passed
  • Fresh `opal seed` runs end-to-end with the new hierarchical templates (246 steps across 6 procedures)
  • `/settings/onshape/configure` flow exercised: save → status flips to ENABLED, TEST CONNECTION with bad keys returns the friendly auth-failed banner
  • Manual: run a real BUILD REPORT on a completed execution
  • Manual: real Onshape keypair, confirm `Connected as ` banner

abbyfluoroethane and others added 30 commits May 12, 2026 13:09
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate all hardcoded CST-100/OPAL references (install scripts, updater,
launcher, README, manual) to amorphous-engineering/OPAL so releases,
install scripts, and the in-app updater point to the correct repo.

Add .github/workflows/ci.yml for lint and test checks on push/PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Suppress F821 (SQLAlchemy forward refs) and B008 (FastAPI Query defaults)
  as false positives in ruff config
- Auto-fix 133 errors: deprecated imports, unsorted imports, unused imports,
  datetime.timezone.utc → datetime.UTC
- Manually fix 34 errors: raise-from in except clauses, unused variables,
  ambiguous variable names, contextlib.suppress, isinstance merging,
  SQLAlchemy .is_(True/False) for boolean comparisons
- Reformat all 93 source files with ruff format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The PowerShell installer now automatically adds the install directory
to the user's PATH instead of just printing instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Derive __version__ from pyproject.toml via importlib.metadata instead of
a hardcoded string — single source of truth, no more drift between
__init__.py and pyproject.toml.

Add tag-driven version step to release workflow so pushing a v* tag
automatically sets the correct version in the built binaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The progress callback in _apply_update was using call_from_thread, but
_apply_update is async and already runs on the app's event loop. Textual
rejects call_from_thread from the app's own thread. Call _log directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The /executions/{instance_id} page used to render every operation and
every sub-step in one long column, with a sidebar that only showed
"OP 1", "OP 2", etc. — no operation name and no way to focus on a
single op.

- Add ?op=<order> query param to executions_detail; default to the
  in-progress op, else the first incomplete op, else the first op.
- Right pane now renders only the selected op's block.
- Sidebar items become 3-line cards: operation name (truncated with
  ellipsis), OP number + progress + status dot, and the
  StepExecution.id as a stable global handle.
- Sidebar clicks do an HTMX partial swap (innerHTML on .exec-main with
  hx-select=".exec-main > *") so window scroll and the sidebar's own
  scroll position are preserved across op switches. hx-push-url keeps
  ?op=N in the address bar; href stays as a fallback for middle-click
  and JS-disabled clients.
- Reserve a transparent 3px left border on every sidebar item so the
  active orange bar doesn't shift the text right when selected, and
  suppress the global [LOADING] ::after on sidebar items so it doesn't
  flicker the row during the request.
Break the ~1200-line execution detail page into a six-tab shell so the
operations checklist, kitting/production, BOM reconciliation, captured
data, issues, and run metadata each have their own focused view instead
of fighting for the same scroll line.

- Tab nav (Meta default, Operations, Data, BOM, Issues, Kitting) uses
  the same hx-get + hx-select partial-swap pattern as the per-op switch,
  with hx-push-url so ?tab=<slug>&op=<order> is bookmarkable.
- Each tab body now lives in src/opal/web/templates/executions/tabs/.
  The shell template keeps the modals and JS once at page level; an
  htmx:afterSwap hook re-runs initExecTab() so attachments, kit
  availability, step-kit availability, and the collaboration singleton
  bind correctly after each swap.
- Visual integration: tab strip mirrors .nav-dropdown-btn (transparent
  1px border that colors in on hover/active, no fills/underlines), runs
  edge-to-edge via negative margins on the strip plus padding-top: 0 on
  .main, and the strip's [LOADING] pseudo is suppressed.
- Page is locked to viewport on the execution-detail page so the tab
  bar stays visible and each tab scrolls within its own content area.
  display: contents on each partial's wrapper lets short tabs stack
  naturally and the operations .exec-layout flex: 1 fill remaining
  space; sidebar and main scroll independently inside the card.
- Operations sub-step rendering prefixes the parent OP number when the
  version's step_number is stored as the sub-step part alone, so step
  headers read "8.5 Verify Charge Continuity" instead of just "5".
  Single-step ops also show their op number on the inner summary row.
- New data_rows precompute in executions_detail() drives the flat
  Data-tab audit table (step / field / value / by / at). Meta extras
  add last_activity_at (max of instance and step-execution updated_at)
  and the procedure-version author.
- Issues tab badges the tab label with the linked-issue count and
  drops the red full-width alert banner.
- Collaboration panel moved from Operations to Meta.
- Status footer is now position: sticky bottom: 0 so it stays at the
  viewport bottom on long pages.
- .exec-layout has a small margin-bottom so the Operations card
  doesn't sit flush against the status bar.
First major release under amorphous engineering.

- Bump version 1.2.8 → 1.3.0 in pyproject.toml + uv.lock.
- OPAL_manual.md: refresh the stale "Version 0.4.5" header.
- Add CHANGELOG.md summarizing the journey v1.2.0 → v1.3.0
  (tabbed execution UI, auto-updater fix, org migration, etc.).
- updater.py: log a warning instead of silently dropping a
  malformed release tag during the version-parse step, so a
  bad upstream tag doesn't quietly disable updates for everyone.
- updater.py: replace_binary() now raises a clear RuntimeError
  pointing at the path and likely cause (Homebrew, /usr/local/bin)
  when rename hits a write-protected location, instead of a
  bare OSError.
- mcp/server.py: replace startup-time print(..., file=sys.stderr)
  calls with logging so MCP logs format consistently.
P0 fixes:
- Setup welcome error message used var(--text-red) which is not
  defined in :root, falling back to currentColor and rendering
  invisible. Switched to var(--accent-red) (welcome/setup.html:44).
- z-index scale was non-monotonic across six magnitudes (10, 100,
  1000, 9000, 9999, 10000). Introduced --z-sticky / --z-dropdown /
  --z-modal / --z-palette tokens and brought sticky footer, ok-modal,
  cmd-palette, and the 14 inline NC/Skip/add-* modals onto the new
  scale. Inline modals were at z=100 (below other 1000-level chrome);
  now correctly layered as modals at z=9000, palette above at z=10000.

P1 cleanup:
- Tokenized --modal-backdrop (rgba 0,0,0,0.8) instead of duplicating
  the literal across 14 modal templates and the cmd-palette/ok-modal
  CSS.
- Tokenized --diff-added-bg / --diff-removed-bg / --diff-modified-bg /
  --diff-changed-bg instead of inlining the same rgba in main.css.
- Off-scale font-size sweep: 0.6875rem / 0.7rem / 0.8rem / 0.85em /
  1.2em / 1.3em / 0.75em / 0.85rem → in-scale 0.75rem / 0.875rem /
  1.25rem across cmd-palette CSS, exec-sidebar-item-gid, and a dozen
  templates.
- Letter-spacing 0.15em (login.html, setup_profile.html) → 0.1em to
  match the brand-link letter-spacing used everywhere else for
  uppercase display text.
- font-weight: bold → 600 across 18 sites for chrome consistency
  with the rest of the app's semibold treatment.

Tab-refactor carry-over:
- Six redundant style="font-size: 0.75rem" overrides on .form-label in
  tabs/operations.html removed (the class already defines that size).
- meta.html step counter font-weight: bold → 600.
- exec-sidebar-item-gid font-size 0.7rem → 0.75rem.
Logging a non-conformance against a step now puts the step into a new
ON_HOLD state. The step stays locked out of normal action paths (no
START/COMPLETE/SKIP) and shows a banner across the top of its content
linking the open NC(s). The step pops back to IN_PROGRESS automatically
the moment the *last* open NC on it reaches a terminal disposition
state (DISPOSITION_APPROVED or CLOSED).

- StepStatus enum gains ON_HOLD.
- POST /steps/{n}/nc transitions the step to ON_HOLD after creating
  the Issue (with audit log).
- POST /steps/{n}/skip refuses ON_HOLD with a clear 400.
- PATCH /issues/{id}: when an NC's status becomes
  DISPOSITION_APPROVED or CLOSED, _maybe_resume_step_after_nc_update
  checks whether any other open NCs remain on the same step
  (step_execution_id); if none, the step is restored to IN_PROGRESS.
- executions_detail() exposes step_holding_ncs[step_execution_id] so
  the template can link directly to the held NCs.
- Operations tab: .step-hold-banner renders at the top of each held
  step's detail body. The buttons row simply has no on_hold branch,
  so START/COMPLETE/SKIP/LOG NC are all suppressed while held.
- Sidebar status dot gets .exec-sidebar-status--on_hold (yellow).
- Status pill ladder extended so 'on_hold' picks up the warn variant.
- 4 new tests in tests/unit/test_execution.py: NC creation puts step
  on hold; sole-NC approval auto-resumes; two NCs keep step held
  until both are resolved; skip rejected while held.
The font tokens promised JetBrains Mono and IBM Plex Sans, but neither
was actually being delivered — nothing in the page loaded them, so the
browser fell back to whatever the OS had (SF Mono / Consolas / Segoe
UI / etc.), and the rendered chrome varied by platform.

Self-host both as variable woff2 files (latin subset) so they ship
with OPAL and don't depend on any external host — keeping with the
local-first / no-cloud-dep posture.

- Add src/opal/web/static/fonts/{ibm-plex-sans,jetbrains-mono}-latin.woff2
  (45 KB + 31 KB; covers weights 100-900 via the variable axis).
- Add @font-face rules at the top of main.css pointing at
  /static/fonts/* with font-weight: 100 900 and font-display: swap.
- Update --font-sans to put 'IBM Plex Sans' first; drop the unused
  'Fira Code' from --font-mono.
- /static is already mounted via FastAPI StaticFiles, so the new
  subfolder is served without any other change.
Now that JetBrains Mono and IBM Plex Sans actually load (last commit),
prose elements that were forced into mono via parent-cascade or
explicit declarations stand out. Tighten the design rule —
mono for data, sans for prose — at the few sites that had drifted.

- main.css: add font-family: var(--font-sans) overrides to .op-title
  and .step-title so op/sub-step titles render as proportional prose
  ("Verify Charge Continuity", "Load Propellants"). Surrounding
  chrome (OP number, status pill, progress, timestamp) inherits mono
  from .op-header / .step-summary as before, matching the hybrid
  layout used by .exec-sidebar-item-title.
- login.html: drop font-family: var(--font-mono) from .login-subtitle
  ("Operations, Procedures, Assets, Logistics") and .login-divider
  ("or") so prose inherits sans from body.
- setup_profile.html: same drop on .setup-subtitle and .setup-email;
  wrap just the email value in <span class="mono"> so "Signed in as"
  prose is sans and the address itself stays mono.
- inventory/table_rows.html: add mono class to the external PN span
  so it matches the internal PN treatment on the same row.
Replace every drag-to-resize textarea in OPAL with a fixed-width
element that grows vertically to fit its content. Width is taken from
the form column the textarea sits in; height tracks scrollHeight on
the fly.

- main.css: every textarea now gets resize: none, overflow: hidden,
  and CSS field-sizing: content. Modern browsers (Chrome 123+, Edge)
  auto-grow purely in CSS.
- layouts/base.html: small JS fallback feature-detects
  CSS.supports('field-sizing', 'content'); if unsupported (Safari,
  Firefox stable today), it listens to body-level 'input' events on
  any textarea and sets el.style.height = scrollHeight + 'px' on each
  keystroke. Runs on DOMContentLoaded and htmx:afterSwap so
  dynamically-inserted textareas (modals, partial swaps) are also
  initialized at their content-fit height.
- procedures/new.html, parts/new.html, parts/edit.html: drop the
  three near-identical inline autoResize() functions, their
  oninput="autoResize(this)" attributes, and the inline
  style="resize: none; overflow: hidden;" overrides. The global
  handler now covers them.
Convert /procedures/{id} from a monolithic single-page editor with
modals + a separate deep-edit page (step_edit.html) to the same
five-tab shell the execution viewer uses. Step authoring now happens
inline in the Operations tab — no more page jumps.

Tabs: Meta / Operations / Kit / Outputs / Versions. Same hx-get +
hx-select + hx-push-url partial-swap shape as executions/detail.html.
Outputs tab renders an empty-state message on non-build procedures
instead of being conditionally absent.

Operations tab is sidebar + main: every op (normal and contingency)
gets a card matching the execution sidebar style (truncated title,
OP# + step count + status dot + global #id). Click an op → main pane
renders just that op. Each sub-step is a <details> row whose body
contains the full inline editor — title, instructions, duration,
sign-off / contingency toggles, step-parts table, and data-capture
schema builder — all of which were previously locked away on
step_edit.html. Saving PATCHes the step and reloads.

Schema state is keyed by step.id in a shared stepSchemas object so
one set of page-level add/edit-field and add-part modals can serve
any expanded step. The schema-builder JS was lifted from step_edit.html
into the shell so nothing else needs to change.

- src/opal/web/routes.py:
  - procedures_detail() takes tab/op/step query params, validates the
    tab, defaults the selected op to the first one. Builds
    step_kit_by_step lookup for the inline editor.
  - procedures_step_edit() is now a redirect to
    /procedures/{id}?tab=operations&op=<parent_order>&step=<step_id>
    so any existing bookmarks still land in the right place with the
    right step pre-expanded.
- src/opal/web/templates/procedures/detail.html: rewritten as tab
  shell (~590 lines, was ~860). Modals (add op, add step, add kit,
  add output, add step-part, add/edit data-capture field) stay at
  page level; included tab partial is /procedures/tabs/<tab>.html.
- src/opal/web/templates/procedures/tabs/{meta,operations,kit,outputs,
  versions,_step_editor}.html: new partials.
- src/opal/web/templates/procedures/step_edit.html: deleted; its body
  is now the included _step_editor.html partial.
- src/opal/web/static/css/main.css: extend body.exec-detail viewport-
  lock + tab strip rules to also cover body.procedure-detail.
- tests/unit/test_procedures.py: 3 new tests — default tab is Meta,
  ?tab=operations renders the sidebar layout with the inline editor,
  and the legacy /steps/{id}/edit URL 302s to the new inline location.

Out of scope (intentional, not touched): drag-and-drop reorder
(reorder API exists but no UI today), inlining version_detail.html as
a Versions sub-pane, and converting add-op/add-step/add-kit/add-output
modal flows from page-reload to fully HTMX-driven swaps.
Procedures are now DAGs, not lists. Authors get drag-to-reorder on
the Operations sidebar and a Resolve-style node-graph in a new Flow
tab; operators get blocked on START when an op's prerequisite ops
haven't reached a terminal status yet.

- step_dependency table (m2m self-join on procedure_step) with
  uq_step_dependency on (step_id, depends_on_step_id). Server-side
  validation: same procedure, top-level ops only, no self-loops,
  no cycles (forward-reachability check before persisting).
- /api/procedures/{id}/dependencies — list edges (GET).
- /api/procedures/{id}/steps/{id}/dependencies — atomic full-set
  replacement (PUT). Audited via log_create / log_delete.
- publish_version() snapshots the dep set as `depends_on: [order, ...]`
  on each op in version.content.steps. Execution always reads from
  the frozen snapshot; runtime edits to the live table don't affect
  in-flight runs.
- start_step() refuses to transition a level-0 op out of PENDING
  while any prereq op in the same instance is not in
  {completed, signed_off, skipped}. 400 with "Cannot start: waiting
  on OP X, Y".
- executions_detail() builds gated_ops_by_order; executions/tabs/
  operations.html shows a yellow BLOCKED chip on the sidebar item
  and replaces the START button with "BLOCKED — waiting on OP X" in
  the main pane while gated. Mirrors the existing ON HOLD treatment.
- New Flow tab (procedures/tabs/flow.html) renders an SVG canvas with
  one node per op auto-positioned by longest-path topological depth.
  Drag from a node's right "output port" to another's left "input
  port" to add a dep; hover an edge to surface a red X marker that
  removes it. Cycle rejection bubbles up to the user via alert.
- Drag-to-reorder in the Operations sidebar (and within an op's
  sub-step list) using HTML5 drag-and-drop; submits to the existing
  /steps/reorder API. window.__allProcedureStepIds is rendered into
  the shell so partial reorders within one container get translated
  back into the full ID list the API requires.
- StepDependency added to db.models.__init__; Alembic migration
  bb285af00e88 creates the table and indexes.
- Five new tests covering dep CRUD, self-loop rejection, cycle
  rejection, snapshot inclusion, and execution-gating enforcement.
HTML5 native drag-and-drop on the Operations sidebar didn't work
because the elements are <a href> (browser intercepts as link drag)
and <summary> (browser intercepts for native disclosure toggle).
The dragstart events either never fired or got swallowed.

Rewrite using pointer events (mousedown / mousemove / mouseup) tracked
manually with a 5px threshold for drag-start. Sidesteps both native
behaviors entirely; same UX (grab the row, drop on another to reorder).

- Replace dragstart/dragover/drop/dragend listeners with a single
  mousedown handler that wires up document-level mousemove/mouseup
  for the duration of the drag.
- Use document.elementsFromPoint() to find the row under the cursor
  during drag (more reliable than the HTML5 drop target lookup).
- Suppress the trailing click event after a drag so the source row
  doesn't navigate / toggle its <details> when the drag ends.
- Idempotent attach via data-reorderBound="1" so the htmx:afterSwap
  re-attach doesn't double-bind.
Previous pointer-event rewrite was the right idea but missed why
mousemove never fired: <a> with an href is natively draggable in
every browser, and the browser starts a link-drag on mousedown-and-
move before our handler can react. While the native link drag is in
progress, the browser doesn't deliver subsequent mousemove events to
our document-level listener — so the threshold check never trips and
the drag never begins. The cursor briefly shows a link-drag ghost and
the row never moves.

- attachReorderHandlers() now sets draggable="false" on every bound
  target and preventDefault()s the dragstart event as a fallback.
  With native link-drag suppressed, the existing pointer-based drag
  code runs end-to-end.

Also fix a separate sub-step reorder bug noticed while re-reading:
- For a <summary> host, host.parentElement is its <details>, not the
  .op-block where sibling sub-steps live. The siblings list came back
  as [host_summary], so sub-step reorder was a no-op.
- Resolve via const hostRow = e.currentTarget.closest('[data-step-id]')
  and use hostRow.parentElement as the container. For sidebar <a>
  this is identical behavior; for sub-step <summary> the container
  correctly becomes the <details>'s parent (.op-block).

Cursor hint moves from el to the row so the grab indicator is on
the element that actually slides during the drag.
Without user-select: none, the browser starts highlighting the row's
text as soon as mousedown+move begins. My threshold-based drag
detector then has no chance to take over — the gesture is interpreted
as a text selection, not a drag.

Set userSelect and webkitUserSelect to 'none' on both the trigger
element and its [data-step-id] row at bind time. Scoped to the bound
rows only, so the rest of the page remains text-selectable.
The reorder API updates ProcedureStep.order (the sequence column),
but procedures_detail() was sorting ops by step.step_number (a
human-facing display label that never changes when ops are
rearranged). So after a successful drag-reorder, the page reloaded
showing the exact same sequence — the new order was persisted but
visually invisible.

Switch both ops.sort() and contingency_ops.sort() to key off
step.order, matching how reorder works.
After dragging an op to a new position, the displayed OP labels now
follow the new order: normal ops renumber to 1, 2, 3...; contingency
ops to C1, C2...; sub-steps inherit their parent's new number
(e.g., a sub-step that was 8.5 becomes 5.5 when its parent OP moves
to position 5).

- _renumber_procedure_steps() helper recomputes step_number for
  every step in a procedure based on the current `order` column.
- reorder_steps() calls it after applying the new order, before
  commit, so a single transaction persists both the sequence change
  and the label update.
- Test that the labels follow the new sequence and that sub-step
  labels track their parent's new prefix.
The yellow chip and action-row callout on ops that are waiting on
prerequisite ops now read "PENDING" instead of "BLOCKED" — matches
how the user thinks about an op that just hasn't reached its turn
yet. The tooltip ("Waiting on OP …") and the inline "PENDING —
waiting on OP X, Y" text keep the explanation, just with a softer
word.
Sidebar row layout was: OP N — progress (right-aligned) — status dot
— PENDING. The PENDING chip pushed against the right edge after the
status dot, where it competed visually with the step count.

Move it before the progress span so the row reads:
  OP N   PENDING    3/5   ●
PENDING sits adjacent to the OP number; progress + status stay on
the right. Both the ops and contingency-ops sidebar blocks updated.
Authors can drag images into the step-instructions textarea (or click
"Insert Image") to upload and embed them as markdown inline; operators
can have steps gated on capturing a photo at runtime, which gets
stored as an Attachment row keyed by step_execution.

- Attachment model gains a nullable procedure_id FK with CASCADE so
  template-level images (inline in step instructions) clean up with
  the procedure. Existing instance / step / issue FKs unchanged.
- /api/attachments/upload now reads procedure_instance_id /
  step_execution_id / issue_id / procedure_id from the multipart
  Form body. They were typed without Form() before, so FastAPI was
  silently treating them as query params and the JS uploads were
  losing the linkage in the form body. Annotating with Form()
  documents the intent and fixes the silent drop.
- Schema builder accepts a new "Photo" field type. No type-specific
  extras; the field serializes as {name, label, type: "photo",
  required} and round-trips through publish snapshots untouched.
- Execution viewer renders photo fields as <input type="file"
  accept="image/*">, which on mobile offers the camera option
  natively. On upload the attachment id is written into a hidden
  [data-capture-field] input so completeStep()'s existing serializer
  picks it up; on submit, data_captured[field.name] is the attachment
  id. Read-only display shows a thumbnail linking to the original.
- Inline images: global JS helpers in layouts/base.html bind to any
  <textarea class="md-image-target">. Drag-drop or the INSERT IMAGE
  button uploads, then splices ![filename](/api/attachments/N/download)
  at the caret. The instructions textarea in
  procedures/tabs/_step_editor.html opts in via the class +
  data-procedure-id attributes.
- marked.js (already loaded) renders the resulting markdown inline.
  New .markdown-content img CSS caps the displayed size.
- Alembic migration eb217417d6d2 adds the procedure_id column and
  index to the attachment table (named constraint so SQLite batch
  ALTER works).
- Tests: upload with procedure_id persists the FK; a photo field
  round-trips through the procedure detail and the published version
  snapshot.
…undary

uploadStepPhoto() was reusing getHeaders() which sets
Content-Type: application/json. When fetch's body is a FormData, the
browser needs to set Content-Type to multipart/form-data with a
generated boundary string — but if the caller already supplied a
Content-Type header, fetch leaves it alone. Server then sees a
multipart body labelled as JSON, can't parse it, and returns 422.

The original uploadAttachment() in the same file avoids this by
building headers manually. Match that pattern for the photo upload.

Also tighten the error message: 422 responses have detail as an array
of objects, which printed as "[object Object]" before. Stringify it
or render as JSON so the actual validation reason is visible.
The "Allow multiple photos" checkbox appears under the field-type
extras when type=photo in the schema-builder modal, controlled by
field.multiple. Single fields keep the same behavior; multi fields
let an operator pick (or repeatedly add) multiple images and remove
any of them before completing the step.

- Schema-builder modal: new extras-photo block with field-multiple
  checkbox. toggleTypeExtras / editField / saveField round-trip the
  multiple flag alongside name / label / type / required.
- data_captured storage normalizes to a JSON array of attachment ids
  (single is a 1-element array). The execution template normalizes
  the read-back via Jinja "is iterable and not string" so older
  scalar-valued fields render too.
- Execution viewer renders a step-photo-gallery: each photo gets a
  thumbnail + REMOVE button (editable state); read-only shows just
  the thumbnails. The hidden capture-field input stores the array as
  JSON; the file <input multiple> is set based on field.multiple.
- uploadStepPhotos handles N-file selection: uploads each image,
  appends to existing array (multi) or replaces (single). New
  removeStepPhoto handler pops an id and re-renders the gallery.
  Build _stepPhotoUploadHeaders() omits Content-Type so the browser
  sets multipart/form-data with the right boundary.
- completeStep recognizes data-capture-field-type="photo" and parses
  the hidden's value as a JSON array, sending null when empty.
  Required-field validation treats an empty array as missing.
- main.css: step-photo-gallery is a wrapping flex row; each
  step-photo-item is a vertical thumbnail + remove-button stack.
- Test updated to round-trip both a single-photo and a multi-photo
  field, asserting the multiple flag persists through publish.
- CHANGELOG.md: fill in the second half of the cycle. The original
  1.3.0 entry stopped at the execution-viewer tabs; this adds the
  procedure-template editor tabbing, drag-reorder + renumbering, the
  Flow tab and operation dependency graph, NC step-hold + auto-
  resume, photo data-capture (single/multi), inline images in step
  instructions, auto-grow textareas, mono/sans rule, font self-
  hosting, plus a "Known limitations" section flagging the inline-
  image post-delete 404, mouse-only drag, and stale OPAL_manual.md.
- routes.py: the captured-data audit table in the Data tab no
  longer prints multi-photo arrays as the literal "[12, 17]". Lists
  render as "2 image(s) (#12, #17)" — single-photo scalar ints are
  unchanged. Schema-agnostic: any list-valued capture gets this
  treatment.
…cution Meta

Authors upload engineering drawings, datasheets, PDFs, etc. to a
procedure template via the Meta tab. Operators see the same list as
download links on the execution Meta page when they run that
procedure. Live read — when the author replaces a drawing, every
in-progress execution sees the new version immediately.

- Attachment model gains a `kind` discriminator column with index.
  'reference' (uploaded via the new docs panel) vs 'inline' (embedded
  in markdown content) vs null (legacy / per-instance attachments).
- POST /api/attachments/upload takes a `kind` form field; GET
  /api/attachments accepts procedure_id + kind query filters so the
  docs panel can pull just its slice.
- Default MIME whitelist expanded to cover the formats engineering
  teams actually carry around — DOCX/XLSX/PPT, DWG, STEP, STL, ZIP,
  JSON, webp, svg, markdown. Existing OPAL_ALLOWED_MIME_TYPES env
  override still wins.
- procedures/tabs/meta.html: new REFERENCE DOCUMENTS panel with an
  UPLOAD button (hidden file picker triggers the existing
  /api/attachments/upload). Each row shows filename → download link,
  MIME type, size, upload timestamp, and an X to delete.
- executions/tabs/meta.html: read-only REFERENCE DOCUMENTS panel
  sitting above the Collaboration + Attachments panels. Same row
  shape minus the delete button.
- procedures_detail() and executions_detail() in routes.py expose
  reference_attachments — Attachment.procedure_id == this AND
  kind == 'reference', ordered by filename.
- base.html: uploadMarkdownImage adds kind=inline so the existing
  drag-image-into-textarea path doesn't pollute the docs list.
- Alembic migration a310ab9da1d8 adds attachment.kind + index.
- Test: upload one inline + one reference on the same procedure;
  GET filtered by kind=reference returns only the reference doc.
abbyfluoroethane and others added 8 commits May 17, 2026 06:32
CI's Format check step (uv run ruff format --check src/) fails on
seven files I touched during the recent feature batch. I'd been
running ruff check (lint) but not ruff format --check (formatter),
so the two drifted apart on master.

ruff format src/ is purely cosmetic — line breaks, trailing commas,
quote normalization. No behavior changes. Tests still 294/294.

Saved a project memory ("Run ruff format alongside ruff check") so
the format gate stays in my muscle-memory checklist.
Previously the dependency-graph layout sorted each column by step number
and placed rows at the column-internal index, so a single-node column
always sat at row 0 even when its parent was several rows down — leading
to long diagonal edges that crossed unrelated rows. Now columns are
ordered by barycenter sweeps and rows are absolute: each node anchors to
the mean row of its prereqs, with monotonic collision avoidance.

Edge routing also auto-pans the canvas when the cursor enters a 40px
band at any edge during a drag, and drops are accepted anywhere on a
node body, not just the 12px left port.
…audit panel fill its section

The audit panel was capped at 300px tall and bounded by an inner
overflow wrapper, leaving dead space in the grid cell whenever the
sibling KEYBOARD SHORTCUTS panel was taller. Removing the wrapper and
bumping the audit limit from 10 to 15 lets the table grow to match the
sibling panel naturally.

Also moved QUICK ACTIONS above the activity / shortcuts row so the
primary CTAs sit closer to the top of the page.
Operators can now insert an ad-hoc operation with disposition sub-steps
into a running execution as part of an NC. The redline rides the NC
disposition for approval, gates the host op until the rework is done,
and shows up in the operations sidebar above its host op with a red
banner linked back to the issue.

Schema: six new nullable columns on step_execution. Non-NULL
ad_hoc_issue_id marks the row as ad-hoc; redline rows carry their own
title/instructions/data-schema/sign-off and a host_order pointing at the
held op. Snapshot rows are unaffected. Numbering is host-relative:
5R1, 5R1.1, 5R1.2.

Gating: start_step blocks a snapshot op while any incomplete redline is
attached to it (orphans from soft-deleted NCs are skipped).
_maybe_resume_step_after_nc_update holds the on_hold step until both
the NC's disposition is terminal and every redline tied to it is
complete. Completing or signing off a redline sub-step re-runs the
auto-resume check.

API: POST/GET/DELETE /api/procedure-instances/{id}/ad-hoc-ops. Delete
is permitted only when no sub-step has started.

UI: red REDLINE banner above each ad-hoc tile in the operations
sidebar, '+ ADD REDLINE OP' button in the held op's main pane when
there are open NCs on it, and a modal in detail.html to author the
redline title and sub-steps.
get_assembly_components and get_assemblies_containing in
src/opal/core/genealogy.py both referenced part.part_number on the Part
ORM model. The model exposes internal_pn (the user-facing project part
number) and external_pn — never part_number. Two call sites updated to
read internal_pn, matching every other use in the codebase.
…step

Two related gating bugs in execution:

1. Logging an NC on a sub-step only held that one step; sibling sub-steps
   in the same op stayed PENDING and were freely startable. Now the
   parent op (level=0) is also flipped to ON_HOLD on NC creation, and
   the auto-resume path propagates the resume back up to the parent
   when its sub-steps are all clear of open NCs.

2. The start-step prereq gate only fired on level-0 ops. Sub-steps of a
   gated op skipped the check entirely, so you could start B.1 before
   OP A finished even though OP B was supposed to depend on A. The gate
   now evaluates against the parent op for sub-steps — same snapshot
   deps, same redline check, plus an explicit refusal when the parent
   op is ON_HOLD.

Operations.html no longer offers a START button on sub-steps whose
parent op is on hold or has unmet prereqs; it shows the reason instead.

Four new tests cover NC propagation up, sibling refusal, resume
propagation, and sub-step refusal under unmet parent prereqs.

Fixes #3
Fixes #2
- BUILD REPORT: new printable /executions/{id}/report page with closeout
  photo support (new attachment kind "closeout", PATCH /api/attachments/{id},
  template/CSS updates, smoke test).
- MCP server: full procedure CRUD (get/update/delete procedure and steps,
  reorder, dependencies, kit/output management, step kit, publish_version,
  list_versions, clone_procedure) plus create_risk. Adds mcp>=1.0 dep.
- opal migrate: honor --project/--database so migrations target the right
  project DB instead of whatever the ambient env points to.
- Seed: rewrite all 6 procedures into operation/step hierarchy (52 ops,
  194 sub-steps with requires_signoff / required_data_schema on
  measurement and verification steps); add default users (admin id=1 plus
  two non-admins); embed parent_step_id, kit_items, and output_items in
  version snapshots so executions render the tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an admin-only /settings/onshape/configure form so Onshape API
credentials can be entered, edited, and tested from the web UI.

- New AppSetting model + migration for runtime-mutable key/value
  config that survives restarts and overrides env values.
- apply_db_overlay() rebuilds the runtime Settings from app_setting
  rows; called at server lifespan startup and after each save.
- OnshapeClient.get_session_info() backs the TEST CONNECTION button;
  bad credentials surface as a friendly "Authentication failed"
  banner instead of a raw JSON decode error.
- Settings index now always shows the ONSHAPE INTEGRATION panel —
  CONFIGURE button when disabled, EDIT alongside PULL/PUSH when
  enabled. Form follows the standard OPAL panel + form_actions
  style and reuses ok.status / var(--accent-*) for messaging.
- Replace broken .alert classes in onshape_documents.html with the
  standard inline-color flash style used elsewhere.
- Bump to 1.3.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@abbyfluoroethane

Copy link
Copy Markdown
Collaborator Author

Closing — released the wrong repo. v1.3.2 release lives in amorphous-engineering/OPAL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant