feat(machines): Machine Settings tab — owner-defined settings sets (PP-43q3)#1388
feat(machines): Machine Settings tab — owner-defined settings sets (PP-43q3)#1388timothyfroehlich wants to merge 49 commits into
Conversation
Adds a new Settings tab to the machine detail page with hardcoded sample data. UI-only scaffold — no schema, no server actions, no persistence. Interactions (expand/collapse, star preferred, baseline select, inline edit open) are all wired client-side with no-op handlers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Updates to Preview Branch (feat/machine-settings-tab-scaffold-PP-43q3) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
There was a problem hiding this comment.
Pull request overview
Scaffolds a UI-only "Settings" tab for the machine detail page with hardcoded sample data — adds the route, the tab strip entry, and a set of presentational client components (set cards, markdown sections, software settings table, dip switch table, baseline selector). No schema, server actions, or permission wiring yet; canEdit is hardcoded true and all save/CTA handlers are no-ops.
Changes:
- New route
/m/[initials]/settingsandSettingsentry inMachineTabStrip. - New
src/components/machines/settings/directory withSettingsTab,SettingsSetCard,MarkdownSection,SoftwareSettingsSection,DipSwitchSection,BaselineSelect. - Uses semantic Material Design 3 tokens (
warning,success,outline-variant) rather than raw palette classes.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/(app)/m/[initials]/(tabs)/settings/page.tsx | New server-component route that resolves the machine and renders SettingsTab with canEdit={true}. |
| src/components/machines/MachineTabStrip.tsx | Adds the settings tab spec after maintenance. |
| src/components/machines/settings/SettingsTab.tsx | Client wrapper holding sample-data state, expand-set, and exclusive preferred-set toggle. |
| src/components/machines/settings/SettingsSetCard.tsx | Expandable card with header (chevron/star/badge/kebab) and sectioned body. |
| src/components/machines/settings/MarkdownSection.tsx | Thin wrapper over InlineEditableField with a no-op save. |
| src/components/machines/settings/SoftwareSettingsSection.tsx | Table of software settings plus an inline baseline editor. |
| src/components/machines/settings/DipSwitchSection.tsx | Table of dip switches with ON/OFF pill styling. |
| src/components/machines/settings/BaselineSelect.tsx | Grouped Select with custom free-text branch via Other.... |
…-tab-scaffold-PP-43q3
CORE-RESP-003 (the new audit:sm-structural lint added in PP-kqbk.6) caught a sm:block on the collapsed-meta string in SettingsSetCard. The card is a self-contained component — its layout decisions should be driven by its own width, not the viewport. Added @container on the header row and switched the meta string from sm:block to @md:block. PP-43q3
…43q3)
Layer-1 statefulness (client-side only, no schema or server actions yet):
- Set kebab → Duplicate now clones with " (copy)" suffix
- Set kebab → Delete confirms then removes
- "+ New set" appends a blank set, auto-expanded
- Star toggle exclusivity preserved
- Click-to-edit set name via new InlineEditableText (commit on
blur/Enter, revert on Esc) — primes the same pattern Layer 2 will
use for table cells
- Baseline picker change persists across collapse/expand
- Markdown surfaces (description / rubbers / post positions / notes)
now persist their edits to local state via onValueChange callbacks
Header reshape per design feedback:
- Description preview is now always visible, lives in the header
(click-to-edit there, removed from the expanded body)
- "updated by X DATE" rendered inline next to the set name; the
separate meta strip ("4 software settings · 3 dip switches") is
gone
- Section counters ("4 settings", "3 switches") dropped from
SoftwareSettings + DipSwitch headers
Dip switches restructured to group by bank:
- New shape: dipSwitchBanks = [{ id, name, switches: [...] }]
- Each bank renders as a native <details>/<summary> Accordion item
(default-open) with the switches as a small table inside
- "+ Add switch" inside each bank + "+ Add bank" below all banks
(UI affordances only — handlers deferred to Layer 2)
Data-shape changes (still scaffold-only):
- description/rubbers/postPositions/notes: string → ProseMirrorDoc | null
- baseline: { group, value } → encoded "Group__Value" string (or
"Other..." / "custom__free-text")
- dipSwitches: flat array → dipSwitchBanks grouped array
Deferred to next pass:
- Inline cell editing for software-settings rows + dip-switch rows
(Layer 2 — blur-save policy still parked at PP-s9xi)
- Wired Add row / Add switch / Add bank handlers
Three asks rolled into one tweak to the description in the header: - Drop the "DESCRIPTION" label above the preview - Smaller font (text-xs) on both the displayed text and the editor - Clicking anywhere on the displayed text enters edit mode (clicks on links inside the rendered markdown still navigate to the link rather than entering edit mode) Built a focused DescriptionInline component rather than extending the shared InlineEditableField — InlineEditableField intentionally keeps its pencil as the sole edit trigger so its content (which can include <a> mentions/links) stays cleanly clickable. The Settings card's header description doesn't need to share that behavior, and diverging the click semantics on the shared component would silently change the four other call sites on the Info tab.
…ker (PP-43q3)
Hardware-section visibility (Option A from session discussion):
- SoftwareSettingsSection auto-hides when softwareSettings is empty
- DipSwitchSection auto-hides when dipSwitchBanks is empty
- Both empty → new HardwareAdjustmentPicker renders with dropdown
("Software setting" / "DIP switch") + a one-liner explaining the
most-machines-have-one-or-the-other rationale
- One missing → direct-button picker ("Add software settings" /
"Add DIP switches")
- Both present → no picker
- Sample data updated to demonstrate all three states: Standard House
(software only), Friday Tournament (software only), new
"Black Knight (early SS demo)" (DIP only, two banks)
Cell-level inline editing (blur-save per PP-s9xi lean):
- New EditableCell primitive: click cell → Input → blur or Enter
commits (calls onCommit only on change), Esc reverts. Tab through
works (browser default — buttons + inputs are focusable). Newly
added rows mount in edit mode with the first cell focused via the
autoFocusOnMount prop.
- Software-settings rows now use EditableCell for ID / Setting / Value.
- DIP-switch entries use EditableCell for Switch and Note. Position is
a binary toggle pill (click to flip ON↔OFF) — no edit mode needed.
Row + bank management:
- "+ Add row" appends a blank software row and focuses its ID cell.
- "+ Add switch" inside a bank appends a blank entry and focuses it.
- "+ Add bank" creates a new bank with auto-name "Bank N" + one blank
switch, focused.
- Trash icon on row hover deletes that row/switch.
- Each bank gets a "Delete bank" button at the bottom of its content
with a confirm dialog.
Sample data now includes _key fields on every row/switch so React
keys stay stable across edits. Duplicate-set also deep-clones rows
with fresh _key values so editing the copy doesn't mutate the original.
Bank rename is deliberately left out of this commit — putting a
click-to-edit button inside the AccordionTrigger (a <summary>) is
fiddly because of the native toggle behavior. It can land in a small
follow-up if you want it before merge.
Layer 1 (header reshape, kebab Duplicate/Delete, click-to-edit title
and description, "+ New set", baseline + markdown persistence) all
unchanged from the prior commit.
…3q3)
Edit-mode gate (replaces always-on click-to-edit):
- SettingsTab gains an isEditMode toggle. A top-of-tab Edit button
(shown only when canEdit — the owner/tech+ permission, still
hardcoded true at the page for the scaffold) flips view ↔ edit.
- Entering edit mode expands every set and keeps them expanded.
- In edit mode the button becomes "Done"; the "New set" button only
shows while editing.
- All mutation affordances are gated on the effective edit state
(canEdit && isEditMode), threaded down as each card's canEdit:
- Set name / description / cells / baseline: read-only in view mode
- Star (Preferred): interactive button in edit mode; in view mode
it's a static gold indicator rendered only on the preferred set
- Kebab (Duplicate/Delete): hidden in view mode
- Add row / Add switch / Add bank / trash / picker: hidden in view
mode (they already keyed off canEdit)
- Empty-state copy adapts: "Click Edit above to add one" in view mode,
"Click New set above" in edit mode, plain text with no permission.
Bank rename:
- DipSwitchSection swapped from the native <details>/<summary>
accordion to a manual one (chevron button owns open/closed state).
This frees the bank name to be an InlineEditableText beside the
chevron — a <summary> toggles on any inner click as a default
action that stopPropagation can't cancel, so the native accordion
couldn't host a click-to-edit name. New onRenameBank handler in
SettingsTab.
Label:
- "Starting from:" → "Initial Install:" on the software-settings
baseline strip.
Not in this commit (pending your call):
- "Other"/baseline picker redesign — bringing datalist vs combobox
options for you to choose.
Per-set edit mode (replaces the global edit toggle):
- Each set card has its own Edit / Done button. Entering edit mode
expands that set and unlocks its content affordances only.
- Set-level operations are always available when canEdit (the
owner/tech+ permission), regardless of edit mode:
- Kebab (Duplicate / Delete)
- Preferred star
- "New set" button (back in the toolbar unconditionally)
- Content editing (name, description, software/dip cells, baseline,
add/delete rows, hardware picker) gates on canEdit && isEditing,
threaded to children as `contentEditable`.
- SettingsTab tracks editingIds (a Set) instead of a single global
isEditMode boolean.
Baseline → shadcn combobox (your pick over native datalist):
- New BaselineCombobox (Popover + cmdk) with Stern / Bally-Williams
group headings, a "Use '<typed>'" custom row, and a persistent
footer hint ("Don't see your install? Just type it in — any text
works") plus a "Pick a preset, or type your own…" search
placeholder to make custom entry obvious.
- baseline is now a plain string (e.g. "Competition Install"); dropped
the old Group__Value / custom__ / "Other..." encoding entirely.
- SoftwareSettingsSection shows the combobox in edit mode and plain
text ("…or 'Not specified'") in view mode.
- Deleted BaselineSelect.tsx (old encoded Select) and
BaselineDatalist.tsx (the comparison loser); removed the temporary
side-by-side comparison block from SettingsTab.
Label: "Starting from:" stays "Initial Install:".
…-tab-scaffold-PP-43q3
Counter-evidence from #1388 (run 27481880209 cold-fail, 27482214047 warm-pass on the SAME :6543 url) shows migrate failure is a cold-start RACE on freshly provisioned branches, not a hard DDL-pooler incompatibility. Add a bounded retry (4 attempts, 10s backoff) around drizzle-kit migrate; migrations are journaled + per-migration transactional so a retry re-applies cleanly. Keeps the tr error visibility and the session-pooler endpoint as defense-in-depth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-9opq) The sticky preview URL was string-templated as pin-point-git-<branch>-advacar.vercel.app. When that label exceeds the 63-char DNS limit (e.g. PR #1388's branch -> 64 chars), Vercel hash-truncates the real alias, so the templated host does not resolve. Capture the alias Vercel actually assigned from the deployment API in wait_for_ready (.alias[] '-git-' entry, else .meta.branchAlias) and use it for PREVIEW_URL, falling back to the template only when the API alias is unavailable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cel alias (PP-l9qb, PP-9opq) (#1538) * fix(ci): run preview migrations via session pooler + surface migrate errors (PP-l9qb) The /preview create step ran 'drizzle-kit migrate' through POSTGRES_URL — the :6543 transaction pooler — which does not reliably support the DDL/prepared statements a migration issues (drizzle.config.ts warns about exactly this). It failed with exit 1 and no surfaced Postgres error on every run; no preview was ever produced. - Route migrations through the SESSION-mode pooler (same host, :5432, IPv4 + DDL-capable) by swapping the port on POSTGRES_URL for POSTGRES_URL_NON_POOLING. - Pipe migrate through 'tr \\r \\n' so drizzle-kit's spinner can no longer bury the real Postgres error behind carriage-return overwrites in the CI log. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ci): retry preview migrate to clear cold-start race (PP-l9qb) Counter-evidence from #1388 (run 27481880209 cold-fail, 27482214047 warm-pass on the SAME :6543 url) shows migrate failure is a cold-start RACE on freshly provisioned branches, not a hard DDL-pooler incompatibility. Add a bounded retry (4 attempts, 10s backoff) around drizzle-kit migrate; migrations are journaled + per-migration transactional so a retry re-applies cleanly. Keeps the tr error visibility and the session-pooler endpoint as defense-in-depth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ci): post Vercel-assigned preview alias, not a templated host (PP-9opq) The sticky preview URL was string-templated as pin-point-git-<branch>-advacar.vercel.app. When that label exceeds the 63-char DNS limit (e.g. PR #1388's branch -> 64 chars), Vercel hash-truncates the real alias, so the templated host does not resolve. Capture the alias Vercel actually assigned from the deployment API in wait_for_ready (.alias[] '-git-' entry, else .meta.branchAlias) and use it for PREVIEW_URL, falling back to the template only when the API alias is unavailable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…-tab-scaffold-PP-43q3
…3q3) Wire seed-machine-settings.mjs into preview-create.sh so the on-demand preview pipeline populates the two AFM showcase settings sets. Drop the localhost-only guard from the seed (it blocked legit ephemeral-branch seeding and was inconsistent with the unguarded seed-users.mjs); the read-only root checkout + AGENTS.md rules already forbid pointing a seed at prod. Follow-up to add a consistent ref-based guard: PP-0abt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
/preview |
Add a machine-level rich-text "How to change settings" block at the top of the Settings tab (coin-door buttons, reset procedure, DIP-switch locations, etc.), shared by every settings set. Stored as a new machines.settings_instructions column and edited inline via the existing InlineEditableField, gated by machines.settings.manage (no timeline event). Removes the per-software-section baselineNote field it replaces (keeps the baseline/"Initial install" picker). Updates the help page, the AFM seed, and the unit/integration tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
/preview extend |
…-tab-scaffold-PP-43q3 # Conflicts: # drizzle/meta/0042_snapshot.json # drizzle/meta/_journal.json
|
/preview |
…PP-43q3) Rework the Machine Settings tab from per-set Edit→Done autosave to an explicit per-unit edit model: - Each (title+content) unit — the set header and each section — has its own Edit / Save / Cancel. Field edits buffer in a working copy and commit atomically per unit: Save merges only that unit's slice onto the committed baseline, so parallel drafts stay isolated (saving one unit never persists another's open edits). Cancel reverts the slice from baseline. - Structural ops (delete, reorder) persist immediately from baseline while preserving other open drafts; add-section creates a draft that commits on its Save; a brand-new set inserts on first Save (temp-id → server-uuid swap). - Per-set serial save queue with out-of-order/clobber protection and temp→UUID rekey; navigation guard scoped to "any unit dirty". - Affordances: full "Edit" button on the header, small icon-only pencils on sections, opaque boxed single-line inputs, rich bodies open straight into the full editor (no mini/full toggle), full-cell desktop click target for table cells, per-section kebab (Delete / Move up / Move down) + desktop-only grip. Removes the old per-field autosave + FieldSaveStatus and the set-level dirty serializer (settings-dirty). jsonb schema and saveSettingsSetAction unchanged. Also bundles in-progress PP-5r0p presets for the machine-level "How to change settings" field (settings-instructions-presets + InlineEditableField wiring), which shares SettingsTab.tsx and could not be committed separately. Checkpoint commit on the feature branch; preflight + unit/integration tests green. Not yet split for review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Conflicts resolved: - Migration meta: regenerated our settings migration on top of main's 0043_productive_cyclops (#1554 tournament-notes removal). The settings table + machines.settings_instructions column is now 0044_chunky_vermin; took main's drizzle/meta and deleted the orphaned 0043_spooky_stone_men.sql. db:reset applies the full chain clean; db:generate reports no schema changes. - MachineTabStrip.tsx: adopted main's new <RouteTabStrip> component and added our "Settings" tab (Info / Settings / Service / Timeline). - machine-event-icons.ts: kept Trash2 (settings_set_deleted icon), dropped Trophy (tournament icon removed upstream, no longer referenced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… on reuse (#1553) Previews served stale schema/seed because `/preview` only ran migrate+seed at comment time and reused a live Supabase branch without resetting it. drizzle migrate skips already-journaled migrations and the seed scripts skip existing rows, so a regenerated migration looked "not applied" and changed seed data never updated (PR #1388). Later pushes never re-ran migrate/seed at all. - Add preview-sync.yaml: on `pull_request` synchronize, path-filtered to drizzle/** + supabase/seed*, re-sync an already-active branch. Exits 0 when no `pr-<N>` branch exists, preserving the zero-cost "no preview by default" model. - Extract the migrate+seed sequence into the shared preview-migrate-seed.sh so the `/preview` and push-resync paths can never drift; PREVIEW_RESET=1 resets before migrate when reusing a live branch. - preview-create.sh resets on the reuse path (manual restart now applies changes). - reset-preview-db.mjs also clears seeded auth.users (seed-users is skip-if-exists and the profile trigger only fires on INSERT, so a public-schema-only reset left broken login state) + a PROD_PROJECT_REF safety guard. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…PP-43q3) Add unit + integration + E2E tests for the per-unit edit / atomic-save model: - save-queue concurrency (coalesce, temp→uuid rekey, error + thrown executor) — new hook unit test - SettingsTab RTL: cancel discards a never-saved set / draft section, required-name and required-title guards block Save, nav-guard arm/disarm + confirm-on-dirty-anchor-click, structural op on a never-saved set is local-only - InlineEditableText / EditableCell: enterkeyhint, Esc-revert, required-error show/clear, codeLike autocorrect/autocapitalize/spellcheck off - integration: 200KB byte-ceiling rejection, duplicate NAME_MAX truncation - E2E: delete-section and reorder survive reload; realign the stale "Done"→"Save" editor-journey assertion the rework outdated No production changes — every component held up under the new tests; only a stale test was realigned to the per-unit Save UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-tab-scaffold-PP-43q3 # Conflicts: # AGENTS.md
…P-43q3, PP-8a5r) PP-8a5r: machine-level "Before you change anything" owner-requests field (settings_requests jsonb column + migration, server action, seed, card wiring). Review remediation (PR #1388 high-effort review), each with a regression test: - fix(A1, data loss): a section saved while a brand-new set's insert is still in flight is no longer dropped — baseline the new set from the sent slice and carry any newer staged payload to the real id. - B1: unsaved-changes nav guard now covers the two always-open machine-level fields (they report dirtiness up via onDirtyChange). - B3/Cl2/Cl5: optimistic value represents clears and reconciles with the server value; isDirty memo via shared docsEqualByText. - A2/I1: optional inline fields can be cleared; required fields show an asterisk placeholder + aria-required. - I2: editable-cell placeholder meets WCAG 1.4.3 contrast. - I3: drop misused title tooltip on the two-tap delete (CORE-A11Y-005). - A4: local-date today(); A3: remove dead save-queue cleanup branch. - Cl3/Cl4: shared guarded ProseMirror docIsEmpty/docsEqualByText helpers. - P3 a11y: edit pencil visible on touch ([@media(hover:none)]); read-only "Add row" uses inert instead of pointer-events-none + tabIndex + aria-hidden. +12 unit/integration regression tests (1296 -> 1308). Follow-ups beaded: PP-cumd (same-set multi-unit coalescing race), PP-annw (variant collapse). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
/preview |
The lockfile carried a stale react@19.2.6 reference after the origin/main merge bumped react to 19.2.7, breaking CI's `pnpm install --frozen-lockfile` (ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY) and skipping all downstream test jobs. Regenerated with `pnpm install`; verified `--frozen-lockfile` now resolves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
/preview |
1 similar comment
|
/preview |
The preview pipeline's 'Seed machine settings demo' step crashed with ENETUNREACH because seed-machine-settings.mjs preferred POSTGRES_URL_NON_POOLING (:5432), which resolves to IPv6 and is unreachable from CI/preview runners. Switch to the pooled POSTGRES_URL (:6543 IPv4) like seed-users.mjs / seed-discord.mjs, per AGENTS.md §7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
/preview |
Implements the Machine Settings tab end-to-end (PP-43q3): owner-defined settings sets (software settings + baseline, DIP switch banks, rubbers/post-positions/notes), with an exclusive Preferred set, persisted and permission-gated, and surfaced on the machine timeline.
Built on the merged precursor #1492 (PP-8oy0,
DEFAULT_TIMELINE_TAGSquery seam) and hardened by a 6-lens adversarial plan review.What's in it
0038):machine_settings_sets— one orderedsectionsJSONB array (mixed-kind discriminated union), JSONBdescription,is_preferred, created/updated by+at; partial unique indexWHERE is_preferred(one preferred per machine); RLS enabled.machines.settings.manage(member→owner, technician/admin→any; viewing public viamachines.view).$type<>is compile-time only) and size caps.baselineNotefield; mentions off; tab order Info · Settings · Service · Timeline.settings_set_*events under a newsettingstag, default-OFF in the filter (and the Info-tab recent activity), toggleable on; filter chips materialize the effective default set so togglingsettingsadds to — rather than narrows to — the view.Tests
23505partial-index backstop, no-op guard, malformed-payload rejection, full permission matrix, and the timeline-emit assertions.userTagSchemaupdates./m/[initials]/settingsadded to responsive-overflow.Local:
check(1214) + full integration (272+106) + production build all green; settings/timeline E2E green. (preflight's smoke step is blocked by the unrelated infra bug PP-yso5 —heavy-run.sh/semword-splits--project='Mobile Chrome'.)