Skip to content

feat: fleet view, migration flow and staged state UI (S2 lane 6b) - #95

Merged
lr00rl merged 7 commits into
feature/20261010_s2-fleet-sourcesfrom
lane/20261016_s2-fleet-view
Oct 17, 2026
Merged

lr00rl merged 7 commits into
feature/20261010_s2-fleet-sourcesfrom
lane/20261016_s2-fleet-view

Conversation

@lr00rl

@lr00rl lr00rl commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Design 28 S2 lane 6b: the Fleet view, the migration flow and the staged state (ui/). Plan sections 6, 7 (lane 6b row) and 11, over the contracts of lanes 4 (plugin methods and structured refusals), 5b (plans, closure, gateway) and 6a (client.ts, fleet.ts). No tag, no release.

What this changes

Staged state in Records. A fleet-bound record's edit waits in staged-v2, so the Published column says both facts: the live share state, and the staged revision over what it waits on (plan pending, not proposed, plan stale, apply failed, plan approved), with both revisions in the proof (title and screen-reader text). Shares that answer the decoy (no identity on a fleet-bound record) and identities that buy nothing (an identity on a non-fleet record) get a line of their own. A legacy record carries an owner credentials badge in the kind cell, and isFlagged reads owner_credentials too, so the attention sort and the side panel and record page flag cover both. A combination flagged both fleet_bound and owner_credentials reads blocked: migrate <member> first.

New verbs, one registry. recordActions.ts gains Publish staged revision, Migrate to fleet…, Share identities… and Discard staged revision, each shown only on a record in the state it is for (applies), with a reason when the session lacks the method. The row menu, the side panel, the record page and the palette all use them; the panel and the page hand plan verbs, and a fleet-bound record's Delete, over to the Records screen, which holds the plan sheet.

  • Publish: plans.propose, then plans.publish in the same click when core answers required: false (a migrated record with no shares publishes from the menu alone). A filed plan, or a plan already waiting on that revision, opens the sheet at its status; shares_without_identity and migration_requires_plan open it at the share identities step.
  • Delete on a fleet-bound row: plans.propose {action: "delete"} with the record's own shares listed as archived; while a combination names the record explicitly the delete is blocked, and "Remove from " opens that combination's editor with the member already removed. A plain delete the plugin refuses (fleet_delete_requires_plan, read from {saved: false, refused}) is said in words with "Delete through a plan" beside it.
  • Discard: a confirmation naming the staged and live revisions and the pending plan it stales, then discard_staged.

MigrationFlow.vue: one stepper for four plan modes (migrate, publish, delete, assign). A migration stages the record and the other legacy members of every combination that reads it (migrate_record, co_migrate), shows the selection, the plugin's warnings and the files that block it (predicted from the list before staging, named by refused.files after), asks for an identity or an archival per share (graph records default to their own identity; an export record has none), warns when an identity holds none of the lines with "Bind identity to this selection", and proposes one plan over every staged record. Status reads planState(): pending with Open in Approvals (/approvals?open=<id>, on the console's allowlist), stale with Propose again (a replaced staged revision and a grown closure included), failed with the record's live revision from list beside it ("was promoted" or "was not promoted"), and a partially applied multi-record plan naming the records that applied, whose shares already serve their identities, with Propose the rest. Every step offers Abandon migration. The plan view lists, per share, what its identity receives and loses and the lines left out grouped by reason with the fix, then the assignments, clearings and archivals. Nothing shows a credential or a share link. Abandon and Next wait while the siblings stage.

Fleet view (FleetScreen.vue), a new layer shown once the handshake declares any S2 method: fleet records and the combinations that gather them on the left; for the chosen record, identities as rows with one bind.preview per identity on expand or Preview all (two at a time, never N×M eagerly), reason chips, exclusion groups with core's fix, the decoy count with Share identities…, the staged revision previewed beside the live one, the record's plan with its status, Create share for this identity (fleet-bound records only) and Bind identity to this selection. The proof line names the catalogue version, the line count, probe availability, the revision previewed and the identity count.

Refusals and the withheld state. Every mutating call reads {saved: false, refused}: delete, migrate_record, discard_staged, import and migrate (per-record staged and refused from an import are said after the count). A legacy record's preview, client output and a file over one show the owner_credentials_withheld notice with Migrate to fleet (WithheldNotice.vue); Fetch now is disabled on such a record under 0.18, and a probe that reaches the guard says the rule, never a provider outage. The record page counts a fleet record's selection steps from fleet_preview ("117 → 23", not "23 → 23").

Overview adds attention items for records that still serve owner credentials (counted), mixed combinations, decoy shares, failed plans and waiting staged revisions.

Carried items

  1. Lane 6a's lines in useSubscriptions.ts are kept and owned: save sends draft.fleet and reads the structured refusal.
  2. Delete reads the structured refusal with refusalMessage (now taking an optional lead, "Not deleted: …"), with a sentence for fleet_delete_requires_plan in en, zh-CN and ru, and tests; migrate_record, discard_staged, import and migrate read it the same way. Restore has no caller in ui/src (no archive surface yet), so there is nothing to wire there.
  3. A refused new record arriving through the catch path now passes the derived id, so it no longer lists itself as another record (planCalls.test.ts, killed mutation 3 below).
  4. Lane 6a's requests: the withheld state on a legacy record's output (TargetSheet, RecordPage nodes and output tabs), the record page Steps count, Migrate to fleet from the withheld notice (also in the editor's preview pane), and the read-scope states (Fleet view denied and readonly).
  5. Lane 5b's notes: delete is propose {action: "delete"} then publish {action: "delete", expected_revision} on required: false; a clear-only plan is from == to with every share_identities value "".

Review fixes (second round, e64c138)

The code review and the design review of e6f76cc, each finding with its fix and its evidence (an e2e or unit test, and a screenshot under the directory below).

Code review:

  • MEDIUM, a failed co-migrated sibling: each sibling's migrate_record failure is kept per record; while any sibling has no staged revision, Next and Propose are blocked and a danger notice names it with core's reason and "Try staging again", which re-runs only the missing siblings (unstagedSiblings in planFlow.ts). Evidence: e2e "a sibling that fails to stage blocks the flow until a retry stages it" over the new harness state ?plans=sibling; unit test; mutation M10; migration-sibling-failed-*.
  • MEDIUM, the masked axe gate: .target-chip-produces drops its opacity: 0.75 (the chip's muted ink already sets it apart), and the withheld-output test runs axe with no exclusion. Evidence: that test at 1440 and 375, light and dark, zero serious; withheld-output-*.
  • MEDIUM, the flow at 375: a viewport capture shows the flow as a full-width sheet over the table; the earlier full-page captures stretched the frame to the table's height and drew the sheet over its top only. e2e/plans.ts now captures the viewport whenever a sheet or dialog is open. The design review below drove both surfaces. Evidence: migration-*-375-*, delete-propose-375-*.
  • LOW, isFlagged only guarded by e2e: unit test for isFlagged and flagOf (owner credentials before the regex label). Mutation M13.
  • LOW, text-derived behaviour: unmatchedNameSteps skips a pattern past 256 characters or with a nested quantifier, reads labels to 128 characters, and its warning now says it is checked in the browser and that the plugin's engine decides. Core's explicit-dependents sentence is parsed by explicitCollectionsFrom, whose unit test pins the sentence from substore_svc_plans.go so a rewording fails a test. Mutation M14.

Design review:

  • High, Live/Staged blanked open rows: switching the revision asks again for every open row, and an open row with no answer yet says it is loading. Evidence: e2e "live and staged: switching asks again for every open row"; mutation M9; fleet-staged-row-*.
  • High, Overview items led to surfaces that read "ok": RecordPlanState.vue, mounted in the side panel and the record page header, shows the staged revision with its proof, the plan with core's state, reason and the live revision verdict (Propose again, Check again, Open in Approvals), the decoy shares with Share identities…, and a mixed combination with Migrate to fleet. Evidence: e2e "an Overview attention item opens a panel and page that show the failed plan" and "a combination core refuses says so in its panel"; mutation M12; overview-failed-panel-*, overview-failed-page-*.
  • Medium, Abandon without confirmation: Abandon migration… sits in its own row under the footer, away from Back, and asks first in the flow, naming each staged revision it drops, the pending plan it stales and the share choices it loses; it is offered at the status too, and for a migration reopened from Publish. Evidence: e2e "abandon migration asks first…" and "abandon is offered at the status too…"; mutation M11; migration-abandon-confirm-*, migration-abandon-status-*.
  • Medium, plain-text warnings: step 1's warnings are a warning notice per record, with "Abandon, then edit the steps of " (the staged revision is built from the record as it is now, so the fix is abandon, edit, migrate again), and each record's warnings are repeated beside it in the propose summary. Evidence: e2e "a carried-over step's warning…" and the main migration test; migration-2-staged-*.
  • Medium, the discard dialog's heading: LtConfirmDialog takes consequencesTitle; discard heads its notes "What happens:" in the body face. Evidence: e2e discard test; mutation M18; discard-confirm-*.
  • Medium, Delete's contradictory copy: a fleet-bound row's Delete title says shares are archived through a plan; the last step is a danger "Delete" with the proof "Core files a plan when a live share names an identity; otherwise the record is deleted now. Either way its shares are archived." Evidence: e2e delete test asserts the title, label, data-variant="danger" and proof; delete-propose-*.
  • Medium, a pending delete plan invisible in the table: the Published cell shows a plan with nothing staged ("delete pending", or "shares change" over the plan state), and the table reads plans for fleet-bound rows and rows whose shares hold an identity they cannot use. Evidence: the delete e2e test; mutation M15; delete-pending-row-*.
  • Medium, Remove from : the editor opens on Content with a notice naming the removed member and the way back (save, publish or approve, then delete again), through a notice slot added to SubscriptionEditor.vue. Evidence: e2e explicit-member test; delete-remove-member-editor-*.
  • Medium, files-blocked guidance: nextBlocked checks the files before "stage first", Stage is disabled after a file refusal, and each predicted file opens its own panel. Evidence: both file e2e tests; mutation M16; migration-blocked-files-*, migration-refused-files-*.
  • Medium, the plan view's hidden disclosure: a chevron on every share row, rows that receive nothing or have lines left out start open, and counts read "N added · N removed · N changed · N left out". Evidence: the main migration test; mutation M17; migration-4-status-*.
  • Medium, inactive identities: collapsed, an inactive identity reads "answers the decoy (expired)", and Create share is disabled with the reason. Evidence: e2e "an expired identity says its share answers the decoy, collapsed, and offers no share"; fleet-inactive-*.
  • Medium, Fleet at 375: choosing a record scrolls the detail into view and focuses it; a row's bound lines and counts stack under the name below 720px. Evidence: e2e "choosing a record brings its detail into view" (focus and in-viewport at 375); fleet-chosen-*.
  • Medium, the status on reopen: a plan reopened without its text says "What the plan does is shown with it in the console's Approvals", steps this sitting never showed are not ticked, and the status names every record the plan promotes (from the text's sections, else from what the flow knows). Evidence: e2e "Publish on a revision a plan waits on opens that plan" and the main migration test (two revision lines); plan-reopened-*.
  • Nits taken: the staged badge's "publish" is "not proposed"; one wording per plan state ("plan stale", "plan approved", "plan rejected"); the Fleet view's staged block says the plan state once; the footer stops repeating what the step's body says; cell sentences use the body face and, stacked at 375, take lines of their own instead of running into the slug; "S3" and "operator decision 8" are gone from operator copy ("a later release"); Bind identity to this selection disables itself once core says unknown method; the identity select shows the chosen identity's status and bound lines under it; keep, clear and archive on an identity-bound share are one radio group; plans joins the harness address params, so ?plans= states survive the first navigation (unit test in pageState.test.ts, mutation M19).
  • Nits not taken: the proof naming both revisions stays in the title and screen-reader text in the table (no focusable badge), and the Fleet view's reason chips still repeat as group headings in an open row.

Files outside this lane touched by the fixes: LtConfirmDialog.vue (one optional prop), SubscriptionEditor.vue (one notice slot), styles.css (the chip opacity), RecordSidePanel.vue and RecordPage.vue (mount RecordPlanState), recordActions.ts (title takes the record), dev/consoleAddress.ts and dev/planHost.ts (harness).

Review fixes (third round, 6b2135a and ba0d55d, tests tightened in 69ac154 and d5e5ab7)

The second design review of e64c138 left six Should-fix findings and nine nits. Each Should-fix is closed with an e2e or unit test and screenshots at 1440 and 375.

  • Focus left behind the sheet when a flow opened from the row menu. The menu closed in the same tick and then put focus back on its trigger after the sheet had focused itself. closeRowMenu now gives focus back only when nothing else has taken it (focus still on <body> or in the vanished menu). A sheet, drawer or output sheet opened from the menu returns focus to the menu's button in the row (overlayTrigger), because the item is gone by then. Evidence: new e2e " from the row menu puts focus in the sheet, and Escape gives it back to the menu" for migrate, delete, share-identities and publish-staged at 1440 and 375, plus discard's dialog; mutations M20 and M28; focus-*.

  • At 375 the first ArrowDown in the Fleet records moved focus to the detail. Arrow keys now keep focus in the radio group. A click or tap, Enter or Space on a record, or a new "Show the identities for " button after the list (visible when focused, below 960px only) brings the detail into view and focuses it. Evidence: e2e "arrow keys walk the records and keep focus there; Enter and the skip button bring the detail" (two ArrowDowns: focus on the third radio, scrollY unchanged); mutation M21; fleet-arrows-*, fleet-skip-375-*, zz-fleet-skip-375-en.png.

  • The Published cell cut off its decoy sentence. The chassis gives a cell's sub-line a minimum width of 126px (.pc-table td small), wider than the column at 1440. small.rec-decoy and small.rec-blocked now take the cell's width and wrap. Evidence: e2e " : the Published cell's sentences stay inside the cell at 1440" in en, zh-CN and ru, light and dark, checking every child rect of td.rec-published and td.rec-kind-cell; mutation M22; records-published-cell-*, zz-published-*.

  • The nowrap "Abandon, then edit the steps of " button scrolled the 375 sheet sideways. It now reads "Abandon and edit steps". The record is named above it in the notice and in the button's accessible name ("Abandon and edit steps: "). settled() now fails when any open sheet's body scrolls sideways, so every state the suites drive checks this. Evidence: the carried-over warning e2e asserts the label and accessible name; mutation M23 (the long label back) fails the main migration test at 375 through the new check; migration-fix-steps-*, zz-fixsteps-* (the sheet body measured 374 of 374 px at 375 in all three locales).

  • A delete on a staged record with a pending promote plan said nothing about either, and the table kept "staged … plan pending". The delete's What changes step now has a "Dropped with it" block: the staged revision is dropped and never published, and the pending plan goes stale once the record is deleted. pendingDelete and rowPlan in planFlow.ts let a pending, stale or failed delete plan win over a staged revision and its promote plan in the table badge, the Fleet view and the record panel and page. deleteState gains stale. Propose again on a delete plan there hands over Delete, not Publish. Evidence: e2e "a delete over a staged record names what it drops, and the table says delete over promote" (the reviewer's steps: remove from the combination, save, delete, propose, close); unit test "lets a delete plan win over a staged revision and its promote plan"; mutations M24 and M26; delete-staged-drops-*, delete-staged-row-*.

  • Abandoning a partly applied plan claimed that the legacy records keep serving. The confirmation now names each combination left blocked, with its shares ("Japan combination stays blocked: … refuses to serve its shares (Japan family link)"), and points to Propose the rest. The line under the button names the combinations that stay blocked. Evidence: e2e "abandoning a partly applied plan names the combination it leaves blocked"; mutation M25; migration-abandon-partial-*.

  • Found by the new cell check on CI, not by the review: in the Linux face the nowrap "owner credentials" badge ("учётные данные владельца") ran 7px past the kind cell on every legacy row in ru. The badge now takes at most the cell's width and wraps (ba0d55d). On macOS it stays on one line. With a wider face forced locally it wraps inside the cell (zz-owner-badge-wide-ru.png). The CI run on ba0d55d is the proof, because the macOS fonts never overflowed.

Nits taken:

  • After Preview all, a revision switch asks again for every row (and reruns if it was still running). E2e "after Preview all…", mutation M27.
  • An archived share is left out of the plan's unbound list. Asserted in the delete-over-staged test, mutation M29.
  • The co-migrated record reads "will be staged with this record" before migrate_record answers. Asserted in the main migration test.
  • Below 480px the sibling retry sits under the reason. The sibling test checks the geometry at 375; see zz-sibling-375-*.
  • A refused.files block names each file once: our sentence plus the list, with core's sentence kept in the title. Asserted in the refused test.
  • The Fleet row's preview error uses the body face.
  • The harness gains ?plans=applied and ?plans=rejected: a plan proposed in the sitting reads as applied or rejected the first time its status is read. E2e "a plan the approver applied, and one the approver rejected"; plan-applied-*, plan-rejected-*.

Nits not taken:

  • The select's option text and the line under it both show the chosen identity's facts. The option text helps choose; the line was added in round two for long names.
  • The record panel's green "ok" is pipe.health for every record, outside this lane.
  • The slug "/openjobs-bob" still breaks at its hyphen (recordTable.ts label and the styles.css wrap rule, both outside this lane).

Files outside the lane touched in this round: none beyond the round-two list (RecordPlanState.vue and PlanStatus.vue are this lane's).

Files in other lanes' or unowned files (smallest changes)

  • ui/src/core-types.ts (6a): regenerated with go run ./tools/tsgen -ref 7ba720658817b959978d3d97fc3077dbf633422c (the server feature head, lanes 5a and 5b merged); contract.test.ts regenerates and matches with LATTICE_SERVER_DIR at that commit.
  • ui/src/fleet.ts (6a): refusalMessage takes an optional lead. ui/src/pipeline.ts (6a): ViewId gains fleet. ui/src/components/SubscriptionEditor.vue (6a): the withheld notice is WithheldNotice (same data-testid, plus Migrate to fleet); the now unused .side-code rule removed. Messages: fleetEditor.refusal.codes.fleet_delete_requires_plan in three locales.
  • Unowned: Shell.vue (the Fleet tab), lensChrome.ts, pageState.ts (the fleet view), recordActions.ts (the S2 verbs, ActionContext, Fetch now withheld), RecordMenu.vue (icons, data-action), RecordActions.vue (hand-over), RecordSidePanel.vue and RecordPage.vue (flagOf, withheld, fleet step counts), TargetSheet.vue (withheld), useSubscriptionOps.ts (import and migrate refusals); tests recordActions.test.ts, commandPalette.test.ts (capability fixtures), subscriptionsList.test.ts, filesList.test.ts (the source scan accepts the shares context); harness dev/fakeHost.ts, dev/fixtures.ts (one hook each).
  • New files beside the plan's four: src/planFlow.ts (the pure rules, tested), src/components/WithheldNotice.vue, src/components/RecordPlanState.vue, src/planFlow.test.ts, src/planCalls.test.ts, dev/planHost.ts, dev/planFixtures.ts, e2e/plans.ts.

Dev harness

?fixture=plans&manifest=s2 serves the plans fixture (asia-fleet staged with a pending plan and a decoy share, fleet records with identity shares, legacy export and graph records, a combination that migrates two graph records together, a file that blocks a migration, a mixed combination after a partial migration, a provider record whose share names an identity). ?plans=denied|large|failed|promoted|partial|stale|closure|bind|sibling|applied|rejected picks core's plan states (sibling: the first migrate_record of the co-migrated sibling fails, a retry stages it; applied and rejected: a plan proposed in the sitting reads as decided the first time its status is read). Revisions are 32 hex characters, as the plugin writes them.

Requests

  • Lattice dashboard: the console's navigation allowlist admits only origin, create and for on /platform/publishing, so "Create share for this identity" cannot preselect the identity; the notice names the identity to choose. An identity parameter there would close it.
  • Lane 5a (tools/tsgen): subStorePlansPublishReply is not generated; the UI types it locally (PublishReply in planFlow.ts).

Mutation checks (each applied to the committed tree, the named tests run, restored with git checkout)

  1. shareFields sends an identity-to-identity move: planFlow.test.ts fails.
  2. Delete ignores the structured refusal: two planCalls.test.ts cases fail.
  3. The catch path passes draft.id.trim() instead of the derived id: planCalls.test.ts fails.
  4. Fetch now offered on a withheld record: the e2e withheld test fails.
  5. holdsNoLine never warns: planFlow.test.ts fails.
  6. Publish proceeds past a filed plan: planCalls.test.ts fails.
  7. Propose reachable with a share undecided: the e2e migration test fails.
  8. A fleet-bound row's Delete as a plain delete: the two e2e delete tests fail.

Second round, on e64c138, each killed:

  1. The Fleet view stops re-asking open rows when the revision switches: the live and staged e2e test fails.
  2. A missing sibling no longer blocks Next: the sibling e2e test fails.
  3. Abandon runs without asking: the abandon e2e test fails.
  4. The side panel drops RecordPlanState: the Overview e2e test fails.
  5. isFlagged ignores owner_credentials: recordTable.test.ts fails.
  6. The nested-quantifier guard returns false: planFlow.test.ts fails (the test's label is short enough to fail rather than hang).
  7. The badge never shows a plan with nothing staged: the delete e2e test fails.
  8. nextBlocked says "stage first" before the files: the files-blocked e2e test fails.
  9. Share rows never start open: the main migration e2e test fails.
  10. The dialog ignores consequencesTitle: the discard e2e test fails.
  11. plans dropped from the harness params: pageState.test.ts fails.

Third round, on 6b2135a and 69ac154, each killed:

  1. closeRowMenu gives focus back even after a sheet took it: 6 of the 10 focus tests fail (migrate, delete, share-identities at both widths).
  2. An arrow key brings the Fleet detail like a click: the arrow-key e2e fails at 375.
  3. The Published cell's sub-line keeps the chassis's 126px floor: all six Published-cell tests fail.
  4. The fix-steps button carries the long label again: the main migration test at 375 fails in settled()'s sheet-width check.
  5. A staged record's row ignores its delete plan: the delete-over-staged e2e fails.
  6. Abandon names no blocked combination: the partial-abandon e2e fails.
  7. A delete names nothing it drops: the delete-over-staged e2e fails.
  8. A revision switch forgets Preview all: the Preview all e2e fails.
  9. A sheet from the menu returns focus to the vanished item: 8 of the 10 focus tests fail on Escape.
  10. Archived shares listed as unbound again: the delete-over-staged e2e fails. Its first assertion survived this mutation, so 69ac154 tightened it.

Tests run

At 6b2135a (UI code), with the test-only commits 69ac154 and d5e5ab7 rerun, and the record-table and cell tests rerun at ba0d55d:

  • npm test: 64 files, 787 passed, 1 skipped (core-types regeneration needs LATTICE_SERVER_DIR).
  • npm run typecheck, npm run build, npm run verify:build: clean (the existing chunk-size warning). The harness typechecks under a scratch config that includes dev/.
  • Playwright, whole suite against a private dev server: 429 passed, 2 skipped (the existing English-only skips). migration.spec.ts and fleet-view.spec.ts: 198 passed, every state at 1440 and 375, light and dark, axe zero serious with no exclusion, the new sheet-width check in every settled(), then zh-CN and ru.
  • GOTOOLCHAIN=go1.26.9 go test -count=1 -run 'Operator|Parity|Vocabulary|UI|Manifest' ./... over system-go/: ok. No Go code changed.
  • CI run 38983503987 on ba0d55d: all jobs green except verify's expected "package twice and compare bytes" step.

At e64c138:

  • npm test: 64 files, 785 passed, 1 skipped (the core-types regeneration check, which needs LATTICE_SERVER_DIR).
  • npm run typecheck, npm run build, npm run verify:build: clean (the existing chunk-size warning). The harness typechecks under a scratch config that includes dev/.
  • Playwright, whole suite against a private dev server: 393 passed, 2 skipped (the existing English-only skips). migration.spec.ts and fleet-view.spec.ts: 162 passed, every state at 1440 and 375, light and dark, axe zero serious with no exclusion, then zh-CN and ru.
  • GOTOOLCHAIN=go1.26.9 go test -count=1 -run 'Operator|Parity|Vocabulary|UI|Manifest' ./... over system-go/ (the tests that read ui/src): ok. No Go code changed.
  • CI run 38944497416 on e64c138: conformance, fuzz, memory, perf and ui-e2e green; verify green through "test UI", "typecheck UI", "build and scan UI", "validate manifest against reviewed server", "build runtime binaries" and "assemble UI bundle tree", then the expected "package twice and compare bytes" failure.

At e6f76cc (first round): 779 unit tests passed, 365 e2e passed, CI run 38932250709 green except the expected "package twice and compare bytes".

Screenshots

Third round, from 6b2135a (later commits change tests, and the badge at ba0d55d, re-rendered in records-published-cell-*): /private/tmp/claude-501/-Users-cdcd-roobli-RTFS-justTaste-Probe-Dashboards-Lattice/27b544c4-30e3-43cb-89f0-aa6a8b2ed3ca/scratchpad/wave3-fix-6b/shots/, 229 files. Every state above is re-rendered at 1440 and 375 in light and dark. New states: delete-staged-drops, delete-staged-row, fleet-arrows, fleet-skip (375), focus-migrate, focus-delete, focus-share-identities, focus-publish-staged, migration-abandon-partial, migration-fix-steps, plan-applied, plan-rejected, records-published-cell (1440, three locales). Close-ups start with zz-: zz-fixsteps-, zz-published-, zz-sibling-375-*, zz-fleet-skip-375-en.

Second round, from e64c138: /private/tmp/claude-501/-Users-cdcd-roobli-RTFS-justTaste-Probe-Dashboards-Lattice/27b544c4-30e3-43cb-89f0-aa6a8b2ed3ca/scratchpad/wave2b-fix-6b/shots/, 166 files named <state>-<width>-<theme>-<locale>.png, each state at 1440 and 375 in light and dark (a viewport capture when a sheet or dialog is open, else the full page): assign-decoy, clear-identity, delete-blocked-member, delete-pending-row, delete-propose, delete-refused-requires-plan, delete-remove-member-editor, delete-through-plan, discard-confirm, fleet-chosen, fleet-denied, fleet-dropped, fleet-empty, fleet-error, fleet-failed-promoted, fleet-inactive, fleet-loading, fleet-previews (also zh-CN, ru), fleet-readonly, fleet-staged-decoy, fleet-staged-row, migration-1-changes, migration-2-staged, migration-3-identities, migration-4-status, migration-abandon-confirm, migration-abandon-status, migration-blocked-files, migration-identities (zh-CN, ru), migration-refused-files, migration-sibling-failed, overview-failed-page, overview-failed-panel, plan-closure-grew, plan-failed (also zh-CN, ru), plan-partial, plan-reopened, plan-stale, plan-too-large, propose-denied, withheld-output.

First round, from e6f76cc: /private/tmp/claude-501/-Users-cdcd-roobli-RTFS-justTaste-Probe-Dashboards-Lattice/27b544c4-30e3-43cb-89f0-aa6a8b2ed3ca/scratchpad/wave2b-6b/shots/ (118 files).

Done

  • Records: staged badge with both revisions, owner-credentials badge and flag, decoy and darkened lines, mixed-combination block
  • Publish (one click or plan), Migrate to fleet, Share identities (assignment-only and clear-only plans), Discard staged, Delete through a plan with the explicit-member block and Remove from
  • MigrationFlow: staging with co-migrated siblings, warnings, file blocks (predicted and refused.files), identities with defaults, zero-bindings warning, propose, plan view with per-share exclusions, every derived status, propose again, propose the rest, abandon
  • Fleet view: records, identities, lazy previews, chips, exclusions with fixes, create share, bind selection, staged and plan, every 6a state that applies
  • Structured refusals on delete, migrate_record, discard_staged, import, migrate; the new-record id fix
  • Withheld state with Migrate to fleet; Fetch now disabled; probe refusal said as a rule
  • Record page Steps count from fleet_preview; Overview attention items; palette entries
  • Unit tests, e2e suites with screenshots, eight mutation checks
  • Second round: every High and Medium finding of the code review and the design review closed with a test and a screenshot, most nits taken, eleven more mutation checks killed
  • CI on e64c138: run 38944497416: conformance, fuzz, memory, perf and ui-e2e green; verify green through every step to "assemble UI bundle tree", then the expected "package twice and compare bytes" failure
  • Third round: all six Should-fix findings of the second design review closed with a test and screenshots at 1440 and 375, seven nits taken, ten more mutation checks killed (M20 to M29)
  • CI on ba0d55d: run 38983503987: conformance, fuzz, memory, perf and ui-e2e green; verify green through "assemble UI bundle tree", then the expected "package twice and compare bytes" failure. The runs on 6b2135a, 69ac154 and d5e5ab7 failed ui-e2e only on the two ru cell tests that ba0d55d fixes.

Not done

  • No design reviewer has driven the third-round fixes (6b2135a and later) yet.
  • Nits left from the second design review: the identity facts appear in both the option and the line under the select; the record panel's "ok" (pipe.health, outside the lane); the slug breaking at its hyphen (outside the lane).
  • bind_selection is not served by core until operator decision 8 is recorded; the UI calls it and says "not available on this server" with a link to vpn-core Users when core answers unknown method.
  • Identity preselection on the console's share form (dashboard allowlist, above).
  • Moving record.fleet into draftFromRecord (lane 6a's optional suggestion) was not taken.
  • migrate_record's warnings are the plugin's English text and are shown as sent.
  • CI's "package twice and compare bytes" step fails by design on lane PRs.
  • Two design nits left as they are: the table's revision proof stays in the title and screen-reader text, and the Fleet view's reason chips repeat as group headings in an open row.

…nge waits on

Design 28 S2 lane 6b, the UI half of plan sections 6, 7 (lane 6b row) and
11. A fleet-bound record's edit waits in staged-v2 until core promotes it,
so the Records table now says both facts in the Published column: what is
live, and the staged revision with what it waits on ("plan pending",
"publish", "plan stale", "apply failed"), with both revisions in the proof.
The row menu, the side panel, the record page and the palette share the
new verbs through the action registry: Publish staged revision (propose,
then publish in one click when no live share in the closure names an
identity; a filed plan or a refusal opens the plan sheet), Migrate to
fleet, Share identities (an assignment-only plan for shares that answer
the decoy, a clear-only plan for an identity that buys nothing) and
Discard staged revision (a confirmation naming both revisions and the plan
it stales). A fleet-bound record's Delete goes through plans.propose
{action: "delete"}, shows the shares it archives, and is blocked with
"Remove from <combination>" while a combination names the record.

MigrationFlow.vue is one stepper for the four plan modes. A migration
stages the record and the other legacy members of every combination that
reads it (migrate_record), shows the selection, the plugin's warnings and
the files that block it (predicted from the list, and named by
refused.files), asks for an identity or an archival per share with graph
records defaulting to their own identity, warns when an identity holds
none of the lines, and proposes one plan. The status step reads
planState(): pending with Open in Approvals, stale with Propose again,
failed with the record's live revision from the list, and a partially
applied multi-record plan with the records that applied and Propose the
rest. Abandon migration discards what the flow staged.

The Fleet view is a new layer: fleet-bound records on the left; for the
chosen one, identities as rows with one bind.preview per identity on
expand or Preview all, reason chips, exclusion groups with core's fix,
Create share for this identity and Bind identity to this selection, whose
unknown-method answer (operator decision 8 not taken) is said as such.

Every refusal a mutating method sends as {saved: false, refused} is read
as one: delete (fleet_delete_requires_plan, with a sentence in en, zh-CN
and ru), migrate_record, discard_staged, import and migrate. A new
record's refusal on the error channel now names the record by the id it
would have had instead of as another record. A legacy record's output,
preview and file output show the owner_credentials_withheld notice with a
Migrate to fleet action, its Fetch now is disabled with the reason, and a
probe that reaches the guard says the rule, never an outage. The record
page counts a fleet record's selection steps from fleet_preview.

core-types.ts is regenerated at lattice-server 7ba7206 for the plan
sections, assignments, archivals and the status row's stale, action and
mode. A pending intent from the palette, a panel or the Fleet view no
longer meets the Records screen's flow state before it is declared.

Constraint: bind_selection is declared by the manifest and not served by
core until operator decision 8 is recorded; the UI says so and links
vpn-core Users.
Rejected: a fifth component for the plan view; PlanStatus.vue carries the
chip and, with detail, the plan text, as the plan's file list allows.
Tested: npm test (65 files, 779 passed, 1 skipped), npm run typecheck,
npm run build, npm run verify:build, contract.test.ts regenerated against
lattice-server 7ba7206.
…hs and themes

The harness gains the plans fixture (?fixture=plans, dev/planFixtures.ts)
and core's plans service, migrate_record, discard_staged, the staged state
the list reports, the structured delete refusal, the owner-credential
withholding of a legacy record's render and probe, and bind_selection
(dev/planHost.ts), all inert without ?manifest=s2. ?plans= picks core's
plan states: denied, large, failed, promoted, partial, stale, closure and
bind. Revisions are shaped as the plugin writes them, 32 hex characters.

migration.spec.ts and fleet-view.spec.ts drive each state of lane 6b's UI
acceptance rule by named elements at 1440 and 375, light and dark, with
axe finding nothing serious, then zh-CN and ru, and write a screenshot per
state when SHOTS_DIR is set. The client sheet's target chips fail colour
contrast on every record, this lane's or not; the withheld-output test
excludes that one selector and says so.

Tested: npx playwright test, whole suite against a private dev server:
365 passed, 2 skipped (the English-only skips); the two new files: 134
passed.
… true

The code and design reviews of lane 6b found states that showed nothing,
or the opposite of the truth, and one migration that could not recover.

A co-migrated sibling whose migrate_record fails is now tracked per record:
it blocks Next and Propose, the flow names it with the reason, and "Try
staging again" re-runs only the missing ones, so a plan can no longer go
out with an empty to_revision. Abandon asks first, in the flow, naming each
staged revision it drops and the pending plan it stales; it sits in its own
slot away from Back and is offered at the status too, also for a migration
reopened from Publish. A carried-over step's warning is a warning notice
with its way out (abandon, then the record's steps open in the editor),
repeated beside the record in the propose summary. A files-blocked
migration says the files in the footer, disables Stage after a file
refusal, and links each file to its panel.

The Fleet view asks again for every open row when Live and Staged switch,
says "answers the decoy (expired)" on an inactive identity and offers it no
share, brings the detail into view on a phone and stacks a row's counts
under its name. The side panel and the record page show the staged
revision, the plan with core's state, reason and the live revision, the
decoy shares and a mixed combination, each with its way out, so the
Overview's attention items no longer lead to a panel that reads "ok".

A fleet-bound row's Delete says shares are archived, and its last step is a
danger "Delete" with the proof that core files a plan when a share names an
identity. A pending delete or share plan shows in the Published cell. Remove
from <combination> opens the members tab with a notice on how to get back
to the delete. The discard dialog heads its notes "What happens:". The plan
view's share rows have a chevron and open when a share receives nothing or
has lines left out, counts are spelled out, every promoted record is
listed, a plan reopened later says its content is in Approvals, and steps
this sitting never showed are not ticked.

Also: the target chip's format label drops its 0.75 opacity, which failed
axe contrast, so the withheld-output test no longer excludes it; the name
check skips patterns past 256 characters or with a nested quantifier and
reads as advisory; core's explicit-dependents sentence is parsed in one
tested place; "plans" rides the harness address so its states survive a
navigation; operator copy drops "S3" and "operator decision 8".

Tested: npm test (785 passed, 1 skipped), typecheck, build, verify:build,
the dev harness typecheck, the whole Playwright suite (393 passed, 2
skipped), the system-go UI-reading tests.
…ete or abandon leaves

The second design review of lane 6b found six problems that remained.

A row menu item that opens a sheet closed the menu, and the menu then put
focus back on its trigger after the sheet had focused itself. Focus ended up
behind the sheet, under the scrim at 375. closeRowMenu now gives focus back
only when nothing else has taken it. A sheet, drawer or output sheet opened
from the menu returns focus to the menu's button in the row, because the
menu item is gone by then.

At narrow widths the first ArrowDown in the Fleet records moved focus to the
detail, and later presses only scrolled the page. Arrow keys now keep focus
in the radio group. A click or tap, Enter, Space, or a new "Show the
identities for <record>" button after the list brings the detail into view.

The chassis gives a cell's sub-line a minimum width of 126px, which is wider
than the Published column at 1440, so the cell cut off the decoy and darkened
sentences. The sub-line now takes the cell's width and wraps.

The "Abandon, then edit the steps of <record>" button did not wrap and was
wider than a 375 sheet. It now reads "Abandon and edit steps". The record is
named in the notice and in the button's accessible name.

A delete on a record with a staged revision now says the staged revision is
dropped and that the pending promote plan goes stale. In the table, the
Fleet view and the record panel, a pending, stale or failed delete plan wins
over the staged revision and its promote plan (pendingDelete, rowPlan).
Propose again on a delete plan opens the delete again, not Publish.

Abandoning a partly applied plan now names each combination that stays
blocked, with its shares, and points to Propose the rest. The line under the
button no longer says that everything keeps serving.

Nits taken: Preview all is asked again for every row after a revision
switch; an archived share is left out of the plan's unbound list; the
co-migrated record "will be staged" before migrate_record answers; the
sibling retry sits under the reason below 480px; each refused file is named
once; the Fleet row error uses the body face. The dev harness gains
?plans=applied and ?plans=rejected.

settled() now also fails when an open sheet's body scrolls sideways.

Tested: vitest 64 files (787 tests, 1 skipped); vue-tsc; dev harness
typecheck; npm run build and verify:build; Playwright fleet-view and
migration specs, 198 passed at 1440 and 375, light and dark, en, zh-CN and
ru.
…ecoy

The first assertion looked for the word archived in the unbound list, a
marker the fix removed along with the archived entries, so it passed with or
without the filter. It now asserts that both shares are archived and that the
unbound list is absent. A mutation that lists archived shares again fails it.
…eet records after arrow keys

The focus and arrow-key tests now leave a screenshot when SHOTS_DIR is set,
so the review can see where focus is at 1440 and 375, not only read it in an
assertion.
CI's ru run of the new cell check found the nowrap badge 7px past the kind
cell on every legacy row: "учётные данные владельца" in the Linux face is
wider than the column at 1440, and the cell clips. The badge now takes at
most the cell's width and wraps to a second line when it must. With a wider
face forced locally it wraps inside the cell; on macOS it stays one line.
@lr00rl
lr00rl merged commit 8c94388 into feature/20261010_s2-fleet-sources Oct 17, 2026
5 of 6 checks passed
@lr00rl
lr00rl deleted the lane/20261016_s2-fleet-view branch October 17, 2026 21:05
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