Skip to content

feat(rollout): rollout framework presentational layer + Storybook - #881

Draft
jmarrxyz wants to merge 31 commits into
mainfrom
feat/rollout-framework-storybook
Draft

feat(rollout): rollout framework presentational layer + Storybook#881
jmarrxyz wants to merge 31 commits into
mainfrom
feat/rollout-framework-storybook

Conversation

@jmarrxyz

@jmarrxyz jmarrxyz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Reviewable diff: +2698/-2 across 18 files (excludes generated, test, and story files).

Summary

Adds a presentational rollout-framework layer + Storybook for fleet rollouts (firmware updates, reboots, firmware release channels), modeled on the shipped curtailment experience. It gives reviewers a runnable reference for the rollout UX direction — the config surfaces, the in-progress lifecycle, and the release-channel management flow — before any of it is wired to live RPCs.

Everything is additive under client/src/protoFleet/features/rollout/, plus one backward-compatible prop on the shared TargetSelectButton. No shipped behavior changes. The components are driven by fixtures and rendered in Storybook, so this is a design-exploration branch, not product code. Unrelated in-flight fleetManagement work in the working tree is intentionally excluded; this branch is rollout-only.

How it works

The layer is a set of presentational React components composed almost entirely from shipped primitives, fed by static fixtures:

  • Config path. A rollout is configured through RolloutControls (strategy / order / batch size / interval), surfaced either in the generic RolloutConfigModal or grafted onto the shipped firmware "Add payload" flow. useRolloutConfigModalState holds the transient form state; rolloutDisplayUtils maps enum values to human labels.
  • Release channels. On the Firmware settings page, a shipped TabStrip adds a Release channels tab beside Files. The tab body is a shared List (ReleaseChannelsTable) with a per-row Manage action that opens ReleaseChannelModal — a full-screen two-pane modal (the shell CurtailmentStartModal uses) with General / Firmware / Applies-to / Rollout on the left and a coverage summary on the right.
  • In-progress path. ActiveRolloutStatus is the in-situ status card (stat grid, progress bar + key, elapsed / est-remaining, excluded-count annotation), mirroring curtailment's ActiveCurtailmentStatus. ActiveRolloutBanner and RolloutPill are the shell-level banner and header pill; ViewRolloutModal is the read-only detail view.
  • In-situ stories. Rather than rendering components in isolation, the stories mount them inside the real app shell + navigation on the pages they'd actually appear on (Firmware settings, Fleet, Energy, Activity), seeding permissions so gated CTAs show. activeRolloutStoryHelpers provides the shared decorators/fixtures for the lifecycle states (scheduled → in progress → paused → pilot review → completed / completed-with-failures) plus a live animated lifecycle.

State lives only in component/store memory for the demo; nothing crosses a network boundary or persists.

Diagrams

flowchart TD
    subgraph Shipped["Shipped primitives (reused)"]
        FSTP["FullScreenTwoPaneModal"]
        LIST["List"]
        TABS["TabStrip"]
        TSB["TargetSelectButton (+ size prop)"]
        PC["ProgressCircular"]
        CSM["CurtailmentStartModal shell"]
    end

    subgraph Rollout["features/rollout (new, presentational)"]
        RC["RolloutControls"]
        RCM["RolloutConfigModal"]
        ARS["ActiveRolloutStatus"]
        ARB["ActiveRolloutBanner / RolloutPill"]
        RCT["ReleaseChannelsTable"]
        RCHM["ReleaseChannelModal"]
        VRM["ViewRolloutModal"]
        FX["fixtures + rolloutDisplayUtils + types"]
    end

    FX --> RC
    FX --> ARS
    FX --> RCT
    RC --> RCM
    RC --> RCHM
    TABS --> RCT
    LIST --> RCT
    FSTP --> RCHM
    CSM --> RCHM
    TSB --> RCHM
    PC --> ARS
    RCT --> RCHM
    ARS --> ARB

    subgraph Stories["Storybook (in-situ)"]
        ST["real AppShell + nav + seeded perms"]
    end
    RCM --> ST
    RCHM --> ST
    ARS --> ST
    ARB --> ST
Loading
stateDiagram-v2
    [*] --> Scheduled
    Scheduled --> InProgress
    InProgress --> Paused
    Paused --> InProgress
    InProgress --> PilotReview
    PilotReview --> InProgress
    InProgress --> Completed
    InProgress --> CompletedWithFailures
    CompletedWithFailures --> [*]
    Completed --> [*]
Loading

Areas of the code involved

Area / file What changed Why it matters for review
components/TargetSelectButton/TargetSelectButton.tsx Added opt-in size prop (defaults to base) Only edit outside features/rollout/. Shared by curtailment + schedule modals; default preserves their rendering.
features/rollout/RolloutControls.tsx, RolloutConfigModal.tsx, RolloutColumnState.tsx, RolloutFieldInfo.tsx, useRolloutConfigModalState.ts New config controls + generic config modal Core reused config surface; check the control set matches the intended strategy/order/batch model.
features/rollout/ActiveRolloutStatus.tsx, ActiveRolloutBanner.tsx, RolloutPill.tsx, ViewRolloutModal.tsx New in-progress status card, banner, header pill, detail modal Mirrors curtailment's ActiveCurtailmentStatus; confirm parity is desirable.
features/rollout/ReleaseChannelsTable.tsx, ReleaseChannelModal.tsx New Files/Release-channels tab table + full-screen Manage/Create modal Composed from TabStrip, List, FullScreenTwoPaneModal, TargetSelectButton, RolloutControls.
features/rollout/rolloutTypes.ts, releaseChannelTypes.ts, rolloutDisplayUtils.ts New types + label/format helpers Display-only; no API contract.
features/rollout/rollout.fixtures.ts, releaseChannel.fixtures.ts Static demo data Drives the stories; not shipped data.
features/rollout/*.stories.tsx, activeRolloutStoryHelpers.tsx Storybook stories + shared decorators (excluded from reviewable count) In-situ renders inside real shell/nav; the intended review surface.

Key technical decisions & trade-offs

  • Presentational + fixture-driven, not RPC-wired. Deliberate: this is a UX reference branch. Landing it as product code is a separate effort.
  • Reuse-first over new patterns. Composes shipped primitives (FullScreenTwoPaneModal, List, TabStrip, TargetSelectButton, ProgressCircular, SecondaryNavigation, CurtailmentStartModal) instead of reinventing. Trade-off: the rollout surfaces inherit those components' behavior/appearance, including the shipped defaults noted below.
  • One additive shared edit. TargetSelectButton gained an opt-in size prop rather than a global restyle, so curtailment/schedule are provably unchanged (default = base; consumer suites green).
  • Curtailment parity for the active-rollout card. Chosen so the two operator experiences read consistently; worth confirming that's the desired direction.
  • Two open UX flags (deliberate, shipped-default): (1) reboot has no page of its own, so its in-situ states render on the Fleet page — a judgment call, easy to relocate. (2) the active TabStrip tab reads orange (shipped TabStrip default, same as FleetLayout/Team tabs) where the mock drew it black; kept the component default rather than override.

Testing & validation

Validated at the pushed HEAD c192222d in a clean worktree pinned to that SHA (working tree carries unrelated in-flight work, so checks were isolated to committed content):

  • tsc --noEmit — clean (exit 0).
  • eslint . --max-warnings 0 (the just lint client step, full client) — clean (exit 0). Covers the client-boundaries import/console rules.
  • Targeted tests — the two existing TargetSelectButton consumers, CurtailmentStartModal.test.tsx + ScheduleModal.test.tsx: 81/81 passing, confirming the additive size prop doesn't regress shipped modals.
  • Hygiene — client-only diff (no proto/db/migration/Go/asicrs/fake-rig skills apply), no console.*, no --no-verify bypass, all 30 commits carry Signed-off-by + Co-authored-by: Jared Marrz.
  • Visual — every story screenshot-verified on Storybook (port 6007): lifecycle states, in-situ surfaces, release-channels tab, Manage modal.

Not run: unit tests for the rollout components themselves (none exist — presentational/fixture-driven) and E2E (slow, no behavioral API change to exercise).

npub1gkwc23nx93g3pmxapjvq077uxelztfzrw9rakg0czrk3k5yajg2qgckw6l and others added 30 commits July 31, 2026 14:34
…ybook

Introduce a process-agnostic `features/rollout/` UI layer for paced,
plan-aware processes (firmware / reboot / curtailment), extracted in
spirit from `features/energy`. Presentational only — driven by fixtures
in Storybook; reconciler wiring is a later phase (see
PLANS/PROTO_FLEET_ROLLOUT_FRAMEWORK.md).

Components (all reuse shared primitives — CompositionBar, Button, Input,
Select, ProgressCircular, PageHeaderPopoverPill):
- RolloutControls: strategy-variant config panel (all-at-once / batched /
  pilot-then-continue), controlled, injected between "Apply to" and
  "Date and time" in a process config modal.
- ActiveRolloutStatus: progress-against-plan card modeled on
  ActiveCurtailmentStatus — composition bar, stat grid, live elapsed
  timer, grouped issues, capability-gated lifecycle actions
  (pause/resume/cancel, pilot continue/retry).
- ActiveRolloutBanner + stack: inline progress banner matching the
  fleet/building banner; stacks per running process on Activity.
- RolloutPill: persistent header pill + popover.
- RolloutColumnState: per-miner phase chip for the process column.

Plus rolloutTypes, rolloutDisplayUtils (phase→CompositionBar mapping,
ETA math), and fixtures. 14 stories under Proto Fleet/Rollout. tsc clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Address design feedback by leaning harder on shipped components:

- RolloutControls: standardize to the single-column field width; move the
  per-strategy helper text out of inline notes into an info popover on the
  strategy field (new RolloutFieldInfo, mirroring CurtailmentStartModal's
  FieldInfoToggle). Paced fields keep curtailment's two-up sub-row.
- ActiveRolloutBanner: rebuild on the shared Callout (our standard inline
  banner) instead of a bespoke banner; intent + process icon per type.
- RolloutColumnState: rebuild on StatusCircle + ProgressCircular + text,
  matching MinerStatus, instead of a bespoke chip.
- ActiveRolloutStatus: mirror ActiveCurtailmentStatus' layout vocabulary
  (section header, big icon + primary lockup, stat-block grid, single
  composition-bar progress section, top-right lifecycle buttons); drop the
  grouped-issues block — the large text, buttons, and progress bar carry
  state and action. No changes to the curtailment implementation.

tsc --noEmit clean. 14 Proto Fleet/Rollout stories still green.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
… in-situ stories

Address the next design pass:

- Auto-retry model (matches curtailment's reconciler): add a `retrying`
  target phase — the analog of curtailment's DRIFTED→redispatch. Transient
  failures self-heal through it; a target only reaches `failed` once retries
  are exhausted. Manual "Retry failed" is kept only on the terminal
  completed-with-failures record. Shown in the progress bar + column state.
- ActiveRolloutStatus: lead the primary lockup with the process *step*
  (rolloutStageLabel — "Batch 5 of 12" / "Pilot review" / "Completed"), with
  the lifecycle state as the eyebrow; miner count demoted to a supporting
  stat. Removed the redundant orange pilot-gate callout — the stage headline,
  stats, buttons, and bar already carry it. Added an `embedded` prop to drop
  the card chrome when hosted in a container.
- ActiveRolloutBanner: neutral/black process icons (intent still tints the
  Callout accent).
- ViewRolloutModal: summon the progress card in a shared Modal(large) over
  the current page — check a rollout without losing context (ActivityDetailModal
  pattern).
- Stories: in-situ set (config modal = Apply to + Rollout + Date and time;
  fleet table with banner + Firmware column; header pill; Activity "Active
  now"; view-rollout modal), plus ViewRolloutModal stories. Dropped unused
  issueGroups from the event model.

tsc + eslint clean. 22 Proto Fleet/Rollout stories.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…ose)

The view-rollout modal rendered as a full-width card pinned to the top with
no dismiss affordance — the card supplied its own section header while the
Modal chrome was suppressed (showHeader=false), so it didn't read as a modal.

- ViewRolloutModal now lets the shared Modal own the header: pass title +
  description (scope), restoring the sticky title bar and the close (X)
  button; dismiss via X / Escape / click-outside as usual.
- ActiveRolloutStatus: when `embedded`, skip its own section header so the
  title isn't duplicated inside the modal body.
- ViewRolloutModal stories: render a stand-in fleet page behind the overlay
  so the modal reads as an overlay over real content.

tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Address modal-rendering feedback:

- New RolloutConfigModal: the bulk-action config surface as a real shared
  Modal (large) — Apply-to scope + RolloutControls + Date-and-time — instead
  of a hand-rolled card. Primary CTA (Start/Schedule rollout) sits in the
  Modal top bar; the body scrolls under the sticky header when it outgrows
  the viewport; dismiss via close / Escape / click-outside.
- ViewRolloutModal: move the lifecycle CTAs (Pause / Cancel remaining /
  Continue / Retry) into the Modal top bar; the embedded card now suppresses
  its own button row via a new `hideActions` prop.
- Extract rolloutLifecycleActions() as the single source of truth for which
  lifecycle controls show, consumed by both the card and the modal so they
  never drift.
- Stories: new Config Modal story; the in-situ config story now uses the real
  modal over a page. Extract useRolloutConfigModalState into its own file
  (keeps the component file fast-refresh clean).

tsc + eslint clean (0 warnings).

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Switch ViewRolloutModal and RolloutConfigModal from the large (1280px) size
to the shared Modal default (standard, 640px). Reflow the ActiveRolloutStatus
stat lockups from a 4-up row to a 2-column grid so Strategy / order / elapsed
/ ETA stay legible at the narrower width (no truncation), and shorten the ETA
label to "Est. time remaining".

tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…odal

- ViewRolloutModal: set forceTitleCollapsed so the title + lifecycle CTAs sit
  in the sticky top bar as a persistent action bar (not only collapsing there
  on scroll).
- RolloutPill: add an onViewRollout callback — "View rollout" becomes an
  in-place button (open the modal) when provided, falling back to the
  detailsPath link otherwise.
- Stories: the pill, single banner, stacked banners, fleet-table banner, and
  Activity "Active now" now each open the ViewRolloutModal in place (per-event
  for the stacks), so every entry-point bucket demonstrates the modal.

tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…tch curtailment colors

Address the active-rollout card feedback:

- Scope: move the orphaned "Applies to <scope>" line into the stat grid as a
  "Scope" StatBlock (mirrors curtailment's "Applies to" stat), and drop it from
  the modal header/description so it isn't duplicated.
- Bar + key: collapse the 5-segment bar (which had two amber buckets) into the
  curtailment shape — Updated (done) / Remaining (in progress + retrying +
  queued) / Failed. New rolloutProgressSegments() replaces the per-phase
  rolloutCompositionSegments(); zero-count buckets drop out. Removed the
  redundant "X of Y updated (%)" line above the bar (now a stat).
- Colors: follow curtailment's curtail-phase precedent exactly
  (curtailProgressColorMap) via a shared rolloutProgressColorMap — done =
  core-primary-fill (not success-green), remaining = core-accent-fill, failed =
  critical — applied to both the CompositionBar and the legend dots.

tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
When embedded in the ViewRolloutModal the status icon butted up against the
sticky header divider. Add pt-2 to the embedded card wrapper so the icon
clears the top bar. Non-embedded (standalone) card padding is unchanged, so
it still matches ActiveCurtailmentStatus' spacing exactly.

tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Bump the embedded card's top gap from pt-2 to pt-6 so the space between the
modal's sticky header divider and the status icon matches the modal's 24px
horizontal inset — balanced margins all around. Non-embedded card unchanged.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
The stat lockups were a hardcoded 2-col grid, so they never reflowed. Mark the
card body `@container` and drive the grid off the card's width (not the
viewport): grid-cols-1 → @xs:grid-cols-2 → @3xl:grid-cols-4. This is the right
axis because the card renders in two very different hosts — the ~592px
ViewRolloutModal (stays 2-up) and a wide standalone detail pane (goes 4-up) —
where viewport breakpoints would misfire. Matches the repo's existing
container-query pattern (CompleteSetup).

tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…nergy, activity)

Per design feedback, show the active rollout UI where it actually lives:
- Firmware settings page: the active rollout card under the "Firmware" page
  header, matching the settings page chrome/insets.
- Energy UI: the rollout card framed like CurtailmentManagementPanel (section
  header + Edit/Run buttons), so a process rollout reads like active
  curtailment does today.
- Activity page: the active-rollout banners stacked in the feed above the
  activity table region, each opening the ViewRolloutModal.

Reproduces the real page shells (Header token + px-10 insets) around the
existing rollout components; no changes to the components themselves.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Per feedback, show the surrounding UI: add an AppShell wrapper that mounts the
real NavigationMenu (w-60 sidebar) with the page content inset beside it, and
apply it to the firmware-settings, energy, and activity in-situ stories. Uses
the same NavigationMenu + primaryNavItems the app uses (same pattern as the
existing NavigationMenu story); no component changes.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…inerStatus

Per feedback, the firmware column state should leverage the states already
built rather than a parallel vocabulary:
- Use the exact in-flight wording MinerStatus shows for these device statuses:
  "Updating firmware" (DeviceStatus.UPDATING), "Rebooting", "Curtailing" — via
  a new columnActiveLabel(processType), instead of the invented "Updating".
- Add deviceStatusToRolloutPhase() documenting how a real integration derives
  the phase from the shipped fleetmanagement.v1.DeviceStatus enum
  (UPDATING/REBOOT_REQUIRED → inProgress, ERROR → failed), so the column can be
  driven from the same status the fleet table reads. The auto-retry "retrying"
  phase has no DeviceStatus analog and comes from the plan rollup.
- RolloutColumnState now takes processType; docs note it renders MinerStatus's
  states, not a separate model.

tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…tories

Per feedback ("seed it, and display the other content even if dummy"):
- AppShell now seeds the fleet store with read permissions (fleet/miner/rack/
  site/curtailment/activity:read) so the permission-gated nav items render —
  the sidebar now shows Home / Fleet / Groups / Energy / Activity / Settings
  instead of just Home + Settings.
- Added representative dummy content around the rollout card on each page via a
  small DummyTable helper: firmware settings gets a firmware-files table,
  Activity gets a "Recent activity" table of completed events below the active
  banners, Energy gets a curtailment "History" table below the active card.

Stories only; no component or production changes.
tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…ption A)

Per the approved option A: a Storybook composition showing the existing "Add
firmware payload" modal (FirmwareUpdateModal) with the rollout framework's
controls added — the firmware-file picker it has today, then the RolloutControls
+ Date-and-time sections below, with the Start/Schedule rollout CTA in the top
bar. Validates the integrated surface without editing the shipped component or
its onConfirm contract (real wiring is a follow-on PR).

Story-only. tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…tories

The in-situ page stories rendered their tables (Firmware files, Recent
activity, curtailment History) as bespoke grid <div>s instead of the
product's real table components. Rework them to reuse what ships:

- Energy History  -> real CurtailmentHistory (its own fixture)
- Activity feed    -> real ActivityTable (ActivityEntrySchema rows)
- Firmware files   -> shared List (the component MinerList/ActivityTable use)
- Fleet table      -> shared List, keeping the RolloutColumnState firmware cell
- Drop the DummyTable helper.

Also add the modal's real upload path to the FirmwareUpdateModal + rollout
story (option A): the 'Upload new file' toggle, product/model/version Inputs,
and the shipped FileDropZone — so the composition reflects that firmware upload
is present in the update-firmware config, not missing.

Story-only. tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
The isolated wrapper/composition story buckets duplicate what the In Situ
collection already shows in real page context:

- View Rollout Modal: shown 5 ways in situ (fleet table, header pill,
  activity active-now, over-the-fleet-page, activity page).
- Rollout Pill: shown by the In Situ header-pill story (popover -> View
  rollout -> modal).
- Config Modal: near-identical duplicate of the In Situ ConfigModal story.
- Active Rollout Banner (single + stacked): shown across the In Situ fleet
  table and activity stories, all three process types.

Kept the atomic component state-matrices the compositions don't fully
exercise: Active Rollout Status (pilot-gate/completed-with-failures),
Rollout Controls (all three strategies), Rollout Column State (all phases).

Story-only. tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
The two fleet-page In Situ stories couldn't be backed by the real fleet
listing without heavy store/protobuf wiring, so they misrepresented the
surface:

- Fleet table: hand-built a List with a bespoke FleetRow type instead of
  the shipped MinerList / minerColConfig.
- View rollout modal (over the fleet page): rendered the fleet page as a
  gray placeholder box, not a real view.

Removed both. The per-miner Firmware column still has a home in the
Rollout Column State bucket; the ViewRolloutModal is still shown in situ
via the header pill and the Activity stories. Dropped the now-orphaned
RolloutColumnState / RolloutTargetPhase / single ActiveRolloutBanner
imports and renumbered the remaining sections.

Story-only. tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Collapse the rollout Storybook down to two buckets per the design steer,
so it reads as a small set of real surfaces plus the isolated parts —
not a sprawl of near-duplicate stories.

In Situ:
- Replace the standalone "Config modal" story with a Bulk actions story:
  the real ActionBar + BulkActionsWidget (the widget miner bulk actions
  use today) hosting Update firmware / Reboot / Curtail. Each opens its
  own config surface — firmware grafts the rollout controls onto the
  shipped "Add firmware payload" modal (composition, no production edit);
  reboot + curtail (no bespoke product modal) use the generic
  RolloutConfigModal.
- Migrate the two states worth keeping from the deleted "Active Rollout
  Status" bucket (paused at pilot gate, completed with failures) onto an
  Activity → Rollout detail surface in the real nav shell.

Components (new RolloutComponents.stories.tsx): the abstracted parts in
one bucket — Rollout controls as a single functional example (flip the
strategy live) + Rollout column state.

Delete the now-redundant standalone story files: ActiveRolloutStatus,
RolloutControls, RolloutColumnState, FirmwareUpdateModalWithRollout.
Add batchedRebootConfig + batchedCurtailmentConfig fixtures for the
generic config modal.

Story-only; no shipped component or contract touched. tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Split the single "In Situ" story bucket into two so launch surfaces and
live surfaces read separately:

- In Situ/Config: the Bulk actions story (launch a process from the
  selection bar).
- In Situ/In Progress: Header pill, Firmware settings, Energy, Activity.

Also, per review:
- Drop the Activity active-now and Activity rollout-detail stories.
- Firmware settings page now renders the real settings subnav (the
  shipped SecondaryNavigation), matching SettingsLayout chrome.
- Reboot bulk modal omits the "Apply to" section — scope is already
  fixed by the selection. RolloutConfigModal.scopeTargets is now
  optional; the Apply-to section renders only when provided.
- Bulk Curtail opens the shipped full-screen CurtailmentStartModal
  instead of the generic config modal.
- Rollout strategy option labels drop "Update" ("All at once",
  "In batches").

Story-only + additive framework tweaks; no shipped component or its
onConfirm contract is edited. tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…ailment

RolloutControls now pairs fields two-up wherever there's a natural
partner (strategy + order, batch size + interval, pilot size + the
offline ceiling), falling back to a lone full-width field — the same
lone-vs-pair reflow curtailment uses.

ActiveRolloutStatus: restore structural parity with the shipped
ActiveCurtailmentStatus, which the previous version had drifted from:
- Standalone card uses the same stat grid (tablet:grid-cols-5,
  gap-x-12) with single-value StatBlocks (Scope, Strategy, Order,
  Est. time remaining) — no more two-line detail lockups.
- Progress section now matches ProgressSection: a summary line
  ("N of M miners updated (P%)") on the left plus a right-aligned
  elapsed readout above the bar, then the CompositionBar, then the
  legend — instead of burying percent/elapsed as stat sub-details.
- In the modal (embedded) the stats render as standard label/value
  table rows (the ActivityDetailModal SummaryRow pattern via the
  shared Row), per review.

Additive rollout layer only; no shipped component edited. tsc + eslint
clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
… active rollout

Mirror shipped curtailment's ActiveCurtailmentStatus in the rollout layer:

- Add an optional onManage handler to RolloutLifecycleHandlers; when
  supplied and the rollout is not terminal, render a leftmost "Manage"
  action (secondary), matching curtailment's Manage gating/ordering.
- Thread onManage through ActiveRolloutStatus and ViewRolloutModal, and
  wire it at every story call site.
- Annotate excluded targets in the progress legend (right-aligned
  "N excluded"), the analog of curtailment's ProgressSection
  "N unavailable" annotation, since excluded targets never appear in the
  composition bar.

Additive story-only rollout framework; no shipped component edited.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…e and reboot

Mirror the shipped ActiveCurtailmentStatus Storybook layout for the two other
active-rollout processes, so each has its own state showcase plus a live
animated lifecycle:

- New ActiveFirmwareRollout.stories (Scheduled / In progress / Paused / Pilot
  review / Completed / Completed with failures + Animated firmware lifecycle).
- New ActiveRebootRollout.stories (In progress / Paused / Completed / Completed
  with failures + Animated reboot lifecycle) — reboot is batched with no pilot
  gate, so it covers only the states it reaches.
- Shared activeRolloutStoryHelpers (ActiveRolloutStatusCard +
  AnimatedRolloutLifecycle) so the two files can't drift, the analog of
  curtailment's AnimatedCurtailmentLifecycle.
- Fixtures for the missing states (scheduled/paused/completed firmware;
  paused/completed/completedWithFailures reboot).

Story-only + additive; renders the shipped ActiveRolloutStatus with fixtures.
No shipped component edited. tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Move the per-state and animated lifecycle stories for firmware and reboot
into the In Situ bucket, rendering each state inside the real app shell:
the NavigationMenu sidebar, a page header carrying the shipped RolloutPill,
and the shipped ViewRolloutModal opened on the rollout over live product
chrome — instead of bare cards on a blank canvas. Firmware and reboot share
a new InSituRollout/AnimatedInSituRollout helper so the two can't drift, and
the animation logic is unchanged.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…established in-situ surface

Replace the rejected header-pill + ViewRolloutModal overlay treatment with the
in-situ surface already established by the In Situ/In Progress "Firmware settings
page" story: ActiveRolloutStatus renders inline in the real page body, not as a
modal over dimmed chrome.

Firmware lifecycle states render on the Firmware settings page (settings subnav +
Firmware header + Upload CTA + firmware files table) via a shared
FirmwareSettingsSurface, with the rollout card inline above the files table. The
shipped FirmwareSettingsPage story now consumes that same surface, so the two
share one source of truth and can't drift.

Reboot has no settings page of its own (it is a Fleet bulk action), so its states
render inline on the Fleet page via FleetSurface, mirroring the firmware inline
treatment. Both surfaces provide their own MemoryRouter, so the lifecycle story
metas opt out of the global StoryRouter (withRouter: false).

Drop the InSituRollout/AnimatedInSituRollout + RolloutPill/ViewRolloutModal
helpers; the animation hook is unchanged. Story-only: no shipped product code is
touched.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Add the firmware Release channels surface to the rollout-framework
Storybook prototype:

- FirmwareSettingsSurface gains an optional Files / Release channels
  TabStrip (shipped Tab component); omitting the tab prop leaves the
  existing firmware-lifecycle stories rendering exactly as before.
- ReleaseChannelsTable: the Release channels tab body — Create CTA over
  the shared List (Name / Miners / Releases / Last updated + a per-row
  Manage action).
- ReleaseChannelModal: the Manage release channel full-screen two-pane
  modal (the shell CurtailmentStartModal uses). Left form (General,
  Firmware file table, Applies-to scope via TargetSelectButton, Rollout
  via the framework's RolloutControls); right pane is the live coverage
  preview (fleet-share ring via ProgressCircular, deploy summary,
  previous rollouts).
- Types + fixtures for release channels, plus stories rendering both the
  tab and the modal in situ on the Firmware settings page.

Presentational only: reuses shipped primitives, no product edits and no
RPC wiring. tsc + eslint clean.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
…g, simplify channel preview

Three UX passes on the rollout Storybook surfaces, all additive under features/rollout/:

- Drop the hand-added "Firmware files" heading above the settings-page files
  table; the shared List already renders its own "N firmware files" count, so
  the heading was a redundant duplicate (matches shipped Firmware.tsx, which
  renders the page Header + List with no heading between them).
- Move the Energy UI story's page-header actions (Edit settings / Run
  curtailment) from base to compact, per the button-sizing rule: base is
  reserved for modal top-bar, dialog, and empty-state actions; page-header and
  toolbar actions use compact.
- Remove the coverage ring and "% of fleet" readout from the Manage release
  channel preview pane; the preview now leads with the deploy summary. Drops the
  now-unused fleetPercent field, fixture value, and ProgressCircular import.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
The firmware rollout Apply-to tables (Manage release channel + the generic
RolloutConfigModal) render their scope-select rows via the shared
TargetSelectButton, which defaulted to the larger base button size. Add an
opt-in `size` prop (default base, so the shipped curtailment and schedule
modals are unchanged) and pass compact from both rollout modals, matching the
button-sizing rule for in-table actions.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
@jmarrxyz
jmarrxyz requested a review from a team as a code owner August 4, 2026 20:06
@github-actions github-actions Bot added javascript Pull requests that update javascript code client review-policy: needs-review Managed by the Review Policy workflow. labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (edf2266530fa0024e298b0583d7626e5404d7908...e476385cb8b9dc06b3065e5a665a19eb26afa3aa, exact PR three-dot diff)
  • Model: gpt-5.6-sol

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: LOW

Findings

[LOW] Completed rollouts use viewing time as their completion time

  • Category: Frontend
  • Location: client/src/protoFleet/features/rollout/ActiveRolloutStatus.tsx:178
  • Description: Elapsed time is always calculated from startedAt to the component's mount-time now. The timer stops for paused or terminal states, but the event model has no ending timestamp. Reopening a completed rollout therefore includes all time since completion.
  • Impact: Historical rollout duration and SLA information becomes increasingly inaccurate, misleading operational investigations.
  • Recommendation: Add an ending or paused timestamp (or authoritative elapsed duration) and use the current time only for actively running rollouts.

[LOW] Scheduling state ignores the plan timestamp and defaults to an expired date

  • Category: Frontend
  • Location: client/src/protoFleet/features/rollout/useRolloutConfigModalState.ts:12
  • Description: The helper ignores initial.scheduledStartAt and always initializes the date to August 4, 2026, which is already in the past at the reviewed commit. This also makes editing an existing scheduled plan display state inconsistent with its configuration.
  • Impact: Operators can be shown the wrong schedule, while a future RPC integration could reject the plan or execute it immediately.
  • Recommendation: Initialize from scheduledStartAt; otherwise use an empty or dynamically future value, and reject missing or past schedules before submission.

[LOW] Device-status mapping treats a firmware attention state as active work

  • Category: Frontend
  • Location: client/src/protoFleet/features/rollout/rolloutDisplayUtils.ts:101
  • Description: REBOOT_REQUIRED means firmware installation finished but requires a reboot, yet the new generic mapper labels it inProgress without considering the rollout process or active batch. It can consequently label an unrelated firmware condition as an active reboot or curtailment.
  • Impact: Per-miner progress may remain stuck or display the wrong operation once this helper is integrated.
  • Recommendation: Derive phases from rollout-target or correlated active-batch state. Use the generated enum and return no rollout phase for unrelated device statuses.

Notes

The change is currently unintegrated Storybook and presentational scaffolding, limiting immediate production exposure. No auth, SQL, command execution, plugin, infrastructure, pool-address, wallet, or protobuf-source changes were present. TypeScript and ESLint could not be run because client dependencies were unavailable; git diff --check passed.


Generated by Codex Security Review |
Triggered by: @jmarrxyz |
Review workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c192222d25

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return "Completed with failures";
case "inProgress":
if (event.strategy === "allAtOnce") {
return "Updating all at once";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the process verb for all-at-once rollouts

When a non-firmware rollout is inProgress and uses the allAtOnce strategy, ActiveRolloutStatus calls this helper and renders Updating all at once. The generic rollout controls expose this strategy for reboot and curtailment as well, so those active rollouts would be mislabeled as firmware updates; derive this branch from event.processType like the other phase labels do.

Useful? React with 👍 / 👎.

@jmarrxyz
jmarrxyz marked this pull request as draft August 4, 2026 20:33
The Add firmware payload modal's payload picker was an always-present inline
radio list that pre-selected the first file and appended an "Upload new file"
button beneath it. That does not scale (the list grows unbounded), assumes a
default selection, and lets both input methods compete at once.

Rework the payload section to:
- use the shipped Select (collapses to one row, scrolls internally when opened)
  instead of an unbounded inline list;
- start with no file selected (placeholder), so the primary action stays hidden
  until the operator chooses a payload;
- switch between "Choose existing" and "Upload new" via the shipped
  SegmentedControl (the DeliveryPicker pattern), so engaging upload demotes/hides
  the file-select method rather than stacking both.

Reuses FileSelectedStatus for the chosen-upload state. Story-only, additive;
no shipped component edits.

Co-authored-by: Jared Marrz <jmarr@squareup.com>
Signed-off-by: Jared Marrz <jmarr@squareup.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client javascript Pull requests that update javascript code review-policy: needs-review Managed by the Review Policy workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant