* refactor(frontend): migrate Stack/Fleet confirms and Git source dialog
- LocalUpdateConfirmDialog -> ConfirmModal with kicker LOCAL · UPDATE
- GitSourcePanel form -> Modal with kicker {STACK} · GIT SOURCE; the
custom three-button footer (Remove / Pull now / Save) stays as-is
since ModalFooter's two-slot pattern doesn't fit a left-aligned
destructive action; ModalHeader provides the canonical chrome
- GitSourcePanel remove confirm -> destructive ConfirmModal with
kicker {STACK} · GIT · DISCONNECT
* test(e2e): scope git-source dialog assertion to the heading
The migration adds a kicker rune "<STACK> · GIT SOURCE" inside the
dialog. The previous locator getByText('Git Source', { exact: false })
matched both the kicker (uppercase, mono) and the italic-serif title,
producing a strict-mode violation. Use getByRole('heading') to target
the title specifically.
Realign tier guards to the user-stated philosophy: Community covers
deploy/monitor at scale plus security basics, Skipper adds automation
and advanced fleet management, Admiral keeps enterprise control.
Community now includes:
- Trivy install / uninstall / update from the Settings Hub (admin role)
- CVE suppressions CRUD (admin role; replicates fleet-wide)
- Manual image scan with vuln, secret, and misconfig results
- Stack-config scan, scan comparison
- Manual fleet snapshots: create, list, view, restore, delete
- Per-node Sencho self-update (Check Updates + per-node Update)
- Fleet Overview search, sort, filters, node-card expand, auto-refresh
Stays paid:
- Scan policies with block_on_deploy enforcement (Skipper+)
- SBOM (SPDX, CycloneDX), SARIF export (Skipper+)
- Bulk Update All across the fleet (Skipper+)
- Scheduled snapshot create (now Skipper, was Admiral)
- Trivy auto-update toggle, fleet-wide policy push (Admiral)
The Settings -> Security tab is unhidden by setting the registry tier to
null. The SecuritySection no longer early-returns a PaidGate; the policy
list, Add Policy button, and policy dialogs are wrapped in {isPaid && }.
The Fleet view drops isPaid gates on the Snapshots tab, Check Updates
button, per-node update handlers, OverviewToolbar grid controls, the
NodeCard expand affordance, and the auto-refresh notice. The
NodeUpdatesSheet receives a canBulkUpdate prop and gates the Update All
button on it. useFleetUpdateStatus and useFleetPolling drop their isPaid
guards so polling runs for Community; useFleetOverview drops the isPaid
wrap on the filter and sort path.
Backend route guards are flipped per the matrix above. The scheduler
tick and requireScheduledTaskTier add 'snapshot' to the Skipper+ branch.
Backend test assertions are inverted for the now-Community endpoints
and a positive Skipper-snapshot-task test is added.
Documentation across features/, api-reference/, and operations/ is
updated to reflect the new tier mapping.
Move the update workflow state machine and the polling driver out of the
FleetView shell into two dedicated hooks under FleetView/hooks/. No
behavior change.
useFleetUpdateStatus owns updateStatuses + updatingNodeId + the four
modal/dialog/reconnecting state slots, the synchronously-held
updateStatusesRef, and every callback that touches the update workflow
(fetchUpdateStatus, triggerNodeUpdate, confirmLocalUpdate,
triggerUpdateAll, dismissNodeUpdate, retryNodeUpdate). The inline four-
line "Check Updates" handler collapses into a single checkUpdates()
callback returned from the hook.
useFleetPolling is a pure side-effect hook that owns the initial-mount
fetch, the paid-tier 30s overview + 120s update-status interval pair,
and the 5s fast-poll accelerator gated on hasUpdatingRef. The polling
hook does not know about update semantics; the consumer passes in
updateStatuses and the fetch callbacks.
Shell drops from 523 to 385 LOC. useState calls drop from 13 to 6,
useEffect from 4 to 0, useCallback from 8 to 2, useRef from 2 to 0.
Tested manually in browser: fleet view loads, masthead populates, Check
Updates opens the sheet and fetches statuses, Refresh fires
fetch-overview, no console errors from the refactor (existing
unreachable-remote-node WebSocket failures are unrelated). Dev servers
killed after validation.
* refactor(frontend): extract useFleetPreferences + useFleetLabels from FleetView (F5-6)
Move localStorage preferences and label palette/assignment fetching out of the
FleetView shell into dedicated hooks under FleetView/hooks/:
- useFleetPreferences: wraps PREFS_KEY, loadPreferences, savePreferences, and
the prefs useState + updatePrefs callback. Defaults are merged on load so stale
stored values cannot produce missing keys. Save is a side-effect-free useEffect
rather than a setState updater call, consistent with React purity contract.
- useFleetLabels: wraps fleetPalette, fleetStackLabelMap, labelFilters state,
fetchLabelsForNodes callback, and the onlineNodeKey-gated fetch effect. The
onlineNodeKey derivation is memoized. labelPaletteKey is exported for the
shell processedNodes useMemo until F5-8 absorbs it.
Shell useState: 17 to 13. useEffect: 5 to 4. useCallback: 10 to 8.
* fix(frontend): add comments to empty catch blocks in useFleetPreferences
Empty catch blocks trigger the no-empty lint rule. Add explanatory comments
to both catch sites to satisfy the rule while keeping the intent clear.
* refactor(frontend): extract NodeCard and OverviewTab from FleetView (F5-3+F5-5)
Folds F5-3 (NodeCard) and F5-5 (OverviewTab) into a single PR since
NodeCard was never previously extracted.
- Move FleetNodeStats, FleetNodeSystemStats, FleetNode into FleetView/types.ts
- Extract NodeCard (~200 LOC) including UsageBar, ContainerRow, StackSection
sub-components and getNodeCpu/getNodeMem/getNodeDisk/isCritical helpers
- Extract OverviewTab (~115 LOC); delegates to NodeCard, OverviewToolbar,
FleetTopology; receives all state as flat props from FleetView
- FleetView.tsx drops from ~1,107 to ~480 LOC (overview inline body gone)
- No logic moved; all state, hooks, and computed values remain in FleetView.tsx
- formatBytes consolidated to @/lib/utils; node helpers exported from NodeCard
- allNodes wrapped in useMemo to prevent unnecessary child re-renders
* fix(frontend): move node utility functions to nodeUtils.ts to fix react-refresh lint error
getNodeCpu, getNodeMem, getNodeDisk, and isCritical were exported from NodeCard.tsx
alongside a React component, violating the react-refresh/only-export-components rule.
Moving them to a dedicated nodeUtils.ts resolves the ESLint error without changing any logic.
Swaps Dialog for Sheet in NodeUpdatesSheet (renamed from
NodeUpdatesModal). Sheet provides full viewport height, removing the
max-h-[85vh] cap on the container and the max-h-[40vh] cap on the node
list scroll area. Width fixed at 700px.
Also fixes two stat-counter bugs carried over from the original inline
code: completed nodes now count toward the Up to date tile, and the
gateway latest-version label now resolves via the local node entry
rather than relying on array position.
No prop or behavior changes.
Moves the inline Node Updates dialog (~192 LOC), Local Update confirmation
dialog (~19 LOC), UpdateStatusBadge sub-component (~58 LOC), and shared
NodeUpdateStatus type into dedicated files under FleetView/. Shell drops
from 1,556 to 1,280 LOC.
Modal-local state (modalSearch, recheckingUpdates) and the
updatableRemoteCount derived value move into NodeUpdatesModal.
Move the inline ReconnectingOverlay sub-component out of FleetView.tsx
into its own file under FleetView/. Pure file relocation; logic and
rendered output are unchanged.
FleetView.tsx: 1,630 -> 1,556 LOC. First step in the FleetView
decomposition tracker.