Commit Graph

19 Commits

Author SHA1 Message Date
Anso adcd04b01a refactor(auto-update): retire per-stack gate, drive auto-update from schedules only (#1233)
* refactor(auto-update): retire per-stack gate, drive auto-update from schedules only

The per-stack Auto-update toggle in the stack sidebar context menu wrote a
gate row to `stack_auto_update_settings`, but actual updates only ran when a
`scheduled_tasks` row with `action='update'` fired. On a fresh install the
toggle was inert: detection ran every 6h, nothing was applied.

The same context menu already exposes `Schedule task`, which opens
ScheduledOperationsView pre-filled for the stack where the user can pick
`Auto-update Stack` and any cron. Keeping the toggle alongside that flow
duplicated the same action and turned the gate table into a parallel store
of "is a covering schedule active" derivable from `scheduled_tasks` itself.

Drop the gate model entirely:
- Backend: remove the `stack_auto_update_settings` table and its four
  accessors, the three routes under /api/stacks/*/auto-update, the per-stack
  skip in /api/auto-update/execute and SchedulerService.executeUpdate's
  fleet branch, and the clearStackAutoUpdateSetting call on stack delete.
  Dashboard `autoUpdate` count derives from scheduled_tasks (action='update'
  rows pinned to the node, total/enabled split).
- Frontend: drop the Auto-update entry from the sidebar context menu and its
  optimistic toggle plumbing. Drop autoUpdateSettings state, the
  /stacks/auto-update-settings fetch, and the auto-update-settings-changed
  WebSocket branch. Slim useSidebarActivitySummary (just nextRunAt; no
  enabled/total counts). AutoUpdateReadinessView's per-card autoUpdateEnabled
  now means "a covering enabled action='update' schedule exists" (per-stack
  row or fleet row on this node, earliest next_run_at wins, per-stack row
  wins on ties), with the gate-fetch removed.
- New: scheduledTasksRouter broadcasts scope: 'scheduled-tasks' on POST,
  PUT, PATCH /toggle, and DELETE so useConfigurationStatus and
  useNextAutoUpdateRun refetch under the 250ms debounce instead of waiting
  for the 60s poll. The broadcast is wrapped so a broken subscriber socket
  cannot turn a successful mutation into a 500.
- Docs: rewrite the "Per-stack control" section of auto-update-policies.mdx
  to describe the schedule-based model; update the matching troubleshooting
  entry. The misleading fleet-update help text in ScheduledOperationsView
  is corrected to reflect that every stack on the node is covered.

Tier parity: the surviving auto-update path (Schedule task -> Auto-update
Stack / All Stacks) is gated `requirePaid + requireAdmin` backend and
`isPaid + isAdmin` frontend, matching the gate the deleted routes carried.
The pre-commit grep returns no tier-related diff outside this PR's scope.

No data migration is provided: greenfield rules apply, and the leftover
table on already-shipped instances is harmless because no code reads or
writes it after this PR.

* docs: sweep remaining references to the per-stack auto-update toggle

The previous commit retired the per-stack Auto-update gate in favor of
configuring auto-update purely through scheduled tasks. This commit
removes the now-stale mentions of that toggle across the operator docs:

- docs/features/sidebar.mdx: drop the Auto-update entry from the Inspect
  group description, the matching screenshot alt-text, and the Skipper
  Note that listed it. Schedule task now carries the cross-link to
  Auto-Update Policies.
- docs/features/stack-management.mdx: drop the Auto-update list item;
  refresh the Schedule task entry to mention the Auto-update Stack action.
- docs/features/dashboard.mdx: rename the Configuration Status row from
  "Auto-update stacks" to "Auto-update schedules" with the new value
  shape, and rewrite the troubleshooting accordion to describe the
  scheduled-tasks invalidation path.
- docs/features/scheduled-operations.mdx: rewrite the Auto-update All
  Stacks row and helper text to reflect that every stack on the node is
  covered (no per-stack opt-out from this surface anymore).
- docs/features/multi-node.mdx: rewrite the Updates column definition to
  derive the Auto/Off flag from enabled Auto-update Stack / Auto-update
  All Stacks schedules instead of the removed per-stack policy.

The auto-update-policies.mdx rewrite in the previous commit already
covered the main reference page. The sidebar-context-menu.png screenshot
will be refreshed on release once the new menu is live in production;
the alt text is updated in this commit so it accurately describes the
shipping state.

No website edits needed: the Auto-Update Policies feature card description
("Schedule automatic image pulls and redeployments per stack on your own
cadence") and the feature matrix labels ("Auto-update stack schedule",
"Auto-update all stacks schedule") remain accurate under the new model.

* fix(stacks): drop orphaned requireAdmin import after auto-update route removal

CI's backend lint step flagged this PR's earlier deletion of the three
/api/stacks/*/auto-update routes: those handlers were the only callers of
`requireAdmin` inside routes/stacks.ts, leaving the named import on line 15
unreferenced. `requirePaid` and `effectiveTier` from the same line are still
in use elsewhere in the file and stay.

tsc --noEmit does not flag unused named imports; ESLint's no-unused-vars
does. Local backend lint reproduces and now reports 0 errors against the
existing 334-warning baseline.
2026-05-26 11:08:33 -04:00
Anso 2a29fed117 feat(labels): harden Stack Labels (gate parity, abort, dry-run, cap) (#1232)
* feat(labels): harden stack-labels surface (gate parity, abort, dry-run policy, cap)

- Cap labelIds array at MAX_LABELS_PER_NODE on PUT /api/stacks/:name/labels.
- Hide sidebar Labels submenu and Settings mutation buttons for roles
  without stack:edit, matching the backend requirePermission gate.
- Break the per-label bulk-action loop on req.aborted so cancelled requests
  release the per-node lock once the in-flight op completes.
- Reset saving state on success in LabelInlineCreateForm so the form stays
  interactive when reused outside the kebab/context menus.
- Invoke enforcePolicyPreDeploy inside the dry-run deploy branch and report
  blocked stacks honestly; previously dry-run skipped the gate and would
  predict success for stacks the real deploy would block.

* fix(labels): split sidebar gate so inline create matches unscoped backend guard

Codex review of #1232 surfaced a parity miss: POST /api/labels is guarded
by unscoped requirePermission('stack:edit'), but the sidebar inline "New
label" entry was gated by canEditLabels (per-stack scoped). An Admiral
user with only scoped grants on a stack could toggle existing labels but
the inline create request would 403.

Splits the sidebar gate:
- canEditLabels (scoped) keeps gating the Labels submenu trigger and toggle
  items, matching PUT /api/stacks/:name/labels.
- canCreateLabels (unscoped) now gates the inline "New label" entry,
  matching POST /api/labels.

Also restores the swallow-catch in LabelInlineCreateForm. The earlier
catch-to-finally change in #1232 let the rethrow from createAndAssignLabel
surface as an unhandled event-handler rejection in the browser console.
Parents already toast on failure; swallowing in the form is the intended
behavior with the finally reset still in place.
2026-05-25 23:48:12 -04:00
Anso 5196f0440e feat(sidebar): surface unreachable nodes in cross-node stack search (#1195)
Per-node fetch failures in useCrossNodeStackSearch were silently
swallowed into []. The user could not tell the difference between
"this node has no matching stack" and "this node timed out or 502'd".

Adds a third return value to the hook: failedNodes: FailedNode[]
where FailedNode is { nodeId, nodeName, reason }. The reason captures
the HTTP status text (e.g. "list returned HTTP 502") or the
underlying Error message ("connect ECONNREFUSED 192.168.x.x:1852")
without leaking node URLs. AbortError from the effect cleanup path
is intentionally excluded - it's expected when the user keeps typing.

Threads the new field through useStackListState and EditorLayout into
StackList. StackList renders a warning chip below the "Other nodes"
header when the array is non-empty:

  ! N nodes unreachable    > (expand)

Click expands the chip to a vertical list of "node: reason" lines.
Hover surfaces the full reasons in the title attribute too, so a
user inspecting at a glance can read them without clicking. The chip
is suppressed entirely when the search yields no failures (no zero
state). Color is the existing warning token, not error - the failure
is recoverable (node may come back) and not a system-wide problem.

GlobalCommandPalette also calls useCrossNodeStackSearch but ignores
the third return value; its destructuring is additive-safe.

Resolves M-2 from the stack-management audit.
2026-05-24 15:40:25 -04:00
Anso bb44db0cb1 refactor(sidebar): rebuild footer as priority-driven Ops Pulse strip (#1178)
* refactor(sidebar): rebuild footer as priority-driven Ops Pulse strip

Replace the simple notification ticker with a derived activity summary that
picks one of six states (active-op, failure, automation, recent-event,
quiet-live, disconnected) and routes per-state clicks to logs, schedules,
or activity. The hook owns the cascade; the component is pure presentation;
EditorLayout owns wiring.

Failure detection covers unread errors in the last 24h; recent-event is
limited to non-error stack notifications in the last hour; automation reads
the next /scheduled-tasks?action=update run and a debounced state-invalidate
listener; the deploy-panel composite key is used for elapsed-time tracking
so close-then-immediately-reopen counts as a new session.

* refactor(sidebar): apply Ops Pulse audit fixes

- countEnabledAutoUpdates now defaults missing autoUpdateSettings entries to
  enabled, matching the backend's getStackAutoUpdateSettingsForNode contract.
  Previously the automation state could not render even with the documented
  per-row default-true.
- findFailure now requires a stack_name so the sidebar does not select a
  system-level error whose click would no-op through navigateToNotification.
  System errors continue to surface via the top-bar NotificationPanel.
- DeployPanelState gains a monotonic sessionId sourced from the existing
  internal counter, and the new usePanelSessionStartedAt hook keys the
  elapsed-time tracker off it so a same-stack rerun always resets even when
  isOpen stays true across succeeded then preparing.
- buildConfig splits quiet-live out of the default and adds an exhaustiveness
  guard so future SidebarActivitySummary variants fail to compile.
- New unit tests cover the default-true aggregation, the same-stack session
  reset, the non-stack failure guard, and the useNextAutoUpdateRun debounce
  and cleanup paths. Frontend suite: 276 / 276 pass.
2026-05-23 15:43:06 -04:00
Anso b52323036b fix: harden stack label permissions (#1036)
* fix: harden stack label permissions

* fix: avoid test db init from debug logging
2026-05-13 11:56:22 -04:00
Anso 166ba21ff1 feat(sidebar): filter toggle + action button padding fix (#933)
* feat: open security basics, manual fleet ops, and basic fleet management to Community

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.

* feat: add node last-contact tracking, fleet latency, and stack-restart summary

- DatabaseService: add last_successful_contact column to nodes table via
  idempotent migration; expose updateNodeLastContact() and getStackRestartSummary()
  methods; include the column in NODE_COLUMNS so getNodes/getNode return it
- fleet.ts: record latency_ms and last_successful_contact on each remote
  node overview fetch; pilot-agent nodes surface pilot_last_seen instead;
  pass db singleton into fetchRemoteNodeOverview to avoid redundant getInstance calls
- dashboard.ts: replace /recent-activity with /stack-restarts endpoint that
  groups notification_history events by stack and category (crash/autoheal/manual)
  over a configurable window (default 7 days, max 30)

* refactor(dashboard): remove redundant per-route authMiddleware

All routes under /api/ are covered by the global auth gate in app.ts.
The inline authMiddleware arguments on /configuration and /stack-restarts
were redundant with that gate and inconsistent with every other route in
the file. Remove them and drop the now-unused import.

* refactor(backend): consolidate Date.now(), move SQL aggregation, normalize node row mapping

- Capture a single completedAt timestamp in fetchRemoteNodeOverview to
  eliminate two separate Date.now() calls and ensure latency_ms and
  last_successful_contact are derived from the same instant
- Inline the redundant contactedAt variable; use completedAt directly
- Move stack-restart aggregation from JS into SQL (GROUP BY stack_name
  with CASE/SUM counts), replacing the Map loop in the route handler
- Export StackRestartSummary interface from DatabaseService and remove
  the duplicate local definition in dashboard.ts; handler now returns
  the query result directly
- Add last_successful_contact normalization in decryptNodeRow, mirroring
  the existing pilot_last_seen pattern
- Add authGate reliance comment above dashboardRouter route handlers

* feat(dashboard): replace Recent Activity card with context-aware Fleet Heartbeat / Stack Restart Map

- Multi-node installs (≥1 remote node): shows Fleet Heartbeat — real-time
  reachability, latency, and container count per registered node
- Local-only installs: shows Stack Restart Map — 7-day restart frequency
  per stack grouped by crash / auto-heal / manual category
- Conditional wrapper (DashboardActivityCard) switches states automatically
  when the node list changes, with no page reload required
- Deletes RecentActivity card and hook (duplicated data already in Recent Alerts)
- Extracts formatRelativeTime to frontend/src/lib/utils.ts for reuse

* fix(dashboard): add pilot_last_seen to FleetNodeOverview and use it in getLastSeenLabel

* fix(fleet): expose mode and pilot_last_seen in overview, consolidate formatRelativeTime, drop em dash

- Add `mode` and `pilot_last_seen` (in seconds) to the FleetNodeOverview
  interface and to both the pilot-agent and HTTP-proxy return paths in
  fetchRemoteNodeOverview so the frontend getLastSeenLabel pilot branch
  can fire correctly
- Remove the private formatRelativeTime from RecentAlerts.tsx and use
  the shared implementation from lib/utils, converting the millisecond
  timestamp at the call site
- Replace the em dash in getLatencyLabel with 'n/a' per project rules

* feat(sidebar): add filter toggle and fix action button padding

- Add collapsible filter chip row in SidebarFilterChips with Plus/Minus
  toggle button pinned to the far right of the row
- Persist expanded/collapsed state across reloads via localStorage key
  sencho:sidebar:filters-visible (default expanded)
- Active filter chip stays applied while the row is hidden; hiding is
  purely a visual noise reduction and does not reset the filter
- Fix flex overflow in SidebarActions by adding min-w-0 to the flex-1
  wrapper around the Create Stack slot, restoring the correct 16px right
  padding on the scan button
- Cap displayed counts at 99+ to bound chip render width; chips use
  min-w-0 overflow-hidden instead of shrink-0 so they flex-shrink
  proportionally rather than hard-clipping the last chip

* test: resolve failing CI tests and act warnings (#933)

- Fix SidebarActivityTicker to properly filter out stale activities
- Update StackGroup test to handle pinned star prefixes
- Refactor useNotifications tests to use waitFor and suppress un-awaitable act() warnings

* fix: add missing beforeAll/afterAll imports to useNotifications test

The tsc build step failed because beforeAll and afterAll were used but
not imported from vitest, unlike the other vitest functions already in
the import statement.

* fix: resolve ESLint errors in test and sidebar files

Replace any[] with unknown[] in useNotifications.test.ts console.error
mock, and add a comment to the empty catch block in StackSidebar.tsx.
2026-05-06 08:43:46 -04:00
Anso 6f45a3b788 fix(frontend): align sidebar brand box with top nav chrome (#928)
* fix(frontend): align sidebar brand box with top nav chrome

Match the TopBar's 56px height so the bottom border of the brand
box meets the bottom border under the nav, removing the 7px seam.
Drop the duplicate uppercase "SENCHO" label and place the version
number baseline-aligned next to the italic wordmark. Center the
brand row, enlarge the logo (28→36px) and wordmark (22→28px), and
mark the logo as decorative since the wordmark already names it.

* test(e2e): key dashboard sentinel off logo src instead of alt text

The brand box change made the sidebar logo decorative (alt=""), since
the adjacent wordmark already names the brand. The Playwright helper
keyed loginAs() off img[alt="Sencho Logo"], so every login wait timed
out and 11 specs failed (with 25 cascading skips via suite teardown).

Switch DASHBOARD_INDICATOR to img[src*="sencho-logo"]: same DOM target,
unaffected by accessibility wording. The selector is unique to the
authenticated sidebar — login/setup screens do not render it.
2026-05-05 07:58:30 -04:00
Anso 1f8ce773ff feat(ui): hide paid features from community-tier dashboard (#891)
* feat(ui): hide paid features from community-tier dashboard

Community installs render only the features they can use. Tier-locked
sections, lock badges, upsell cards, and "Upgrade" buttons no longer
appear anywhere except the License page in Settings, which is the
single discoverable upgrade path.

Concretely:

- PaidGate and AdmiralGate now render null for non-qualifying tiers
  instead of upsell cards.
- SectionGate (settings) hides tier-locked sections entirely.
- Settings sidebar and command palette filter out items the operator
  cannot reach.
- Configuration Status widget on the dashboard drops the Automation
  section for community and hides any locked rows in remaining
  sections.
- Fleet > Status node cards drop locked summary rows.
- Stack action menu, sidebar bulk bar, file upload / download, scan
  comparison, network topology toggle, node label picker all hide
  for community instead of showing disabled affordances or "Upgrade"
  literal text.
- Removes tierUpsell, TierLockChip, and useDismissalState (no longer
  referenced).

Backend tier guards remain authoritative; this changes UI discovery
only.

* test(e2e): assert upload control is absent in community tier

The community-clean-ui change removes the "Upgrade to unlock upload"
pill from the file explorer. Update the matching e2e assertion to
verify the upload control is not rendered, instead of waiting for a
pill that no longer exists.
2026-05-03 01:17:02 -04:00
Anso f62716f557 refactor(design): align typography, colors, and card surfaces to DESIGN.md (#859)
* refactor(design): align surface tokens to DESIGN.md §2

* refactor(design): canonicalize tracked-mono kickers and display rungs

* refactor(design): collapse to five-slot palette and align card surfaces
2026-05-01 03:01:00 -04:00
Anso a0bf5b5bf5 feat(sidebar): bulk stack operations (#854)
* feat(sidebar): bulk stack operations (select, start/stop/restart/update)

- Add ⊞ bulk mode toggle in SidebarActions (cyan active state, tooltip "Bulk
  mode (B)"); keyboard shortcut B toggles, Esc exits, Ctrl+A selects all
  visible (chip-filtered) stacks
- Reserved checkbox column in StackRow becomes visible and interactive in bulk
  mode; clicking a row in bulk mode toggles selection instead of opening the
  stack; kebab and context-menu still work in either mode
- SidebarBulkBar appears below filter chips when >=1 stack selected: shows
  count, Start / Stop / Restart / Update actions; Update is disabled with a
  Skipper TierBadge for Community licenses
- useBulkStackActions hook fans out operations via Promise.allSettled and
  surfaces an aggregate toast ("3 of 4 restarted; 1 failed: plex")
- Bulk update enforced Skipper-gated frontend-side (isPaid check in hook) and
  sends x-bulk-mode header for backend defense-in-depth
- Extract isInputFocused / isPaletteOpen to lib/keyboard-guards.ts; both
  useStackKeyboardShortcuts and the new bulk keyboard effect now share the
  same guards instead of duplicating the logic
- chipFilteredFiles captured via useRef in bulk keyboard effect so the listener
  is not torn down and re-added on every status-poll cycle

* fix(sidebar): separate TooltipProviders for bulk and scan icon buttons

Wrapping both icon buttons in a single TooltipProvider made them
render as one flex child, collapsing the gap-2 between them.
Splitting into two independent TooltipProviders restores the 8px
gap and right padding of the scan button.
2026-04-30 19:53:12 -04:00
Anso 4c0efcb9a8 feat(sidebar): §14 sidebar orchestration, filter chips, pinned rail, trailing column (#850)
* feat(sidebar): §14 sidebar orchestration (filter chips, pinned rail, trailing column)

- Add All / Up / Down / Updates filter chips with live counts; active chip
  filters the list; chip-filtered files computed in EditorLayout with useMemo
- Surface the PINNED group with a 3px cyan left rail and glow via the
  sidebarPinnedGroupRail token; reuses the brand token already on the active row
- Compact brand row from three stacked elements to a single 44px horizontal bar
- Restructure StackRow trailing column as fixed slots: label dots (max 3 + +N
  overflow), update-dot | git-pending icon (priority order), kebab; add reserved
  invisible checkbox slot for PR2 bulk mode
- Export statusText / statusColor from StackRow and reuse them in StackList
  remote-results section to remove the duplicate inline logic
- Lift filterChip state and chip-filtered files to EditorLayout; remove
  filterChip from StackListProps to eliminate the dual-path redundancy
- Remove unused labels param from buildGroups and the void labels workaround
- Wrap filteredFiles in useMemo so filterCounts memo is not defeated on every
  render

* fix(sidebar): move statusText and statusColor to stack-status-utils

react-refresh/only-export-components requires component files to export
only components. Move the two utility functions and StackRowStatus type
to a dedicated stack-status-utils.ts so StackRow.tsx is a pure component
module. Update StackList.tsx and EditorLayout.tsx to import from the new
source directly.
2026-04-30 19:37:49 -04:00
Anso f94b2ce85c fix(sidebar): remove 1-hour staleness filter from activity ticker (#786)
Show the most recent stack event regardless of age. The ticker now
stays populated as long as any event arrived in the current session,
with formatTimeAgo reflecting the true elapsed time (e.g. '2h ago').
Idle state only appears when no events have arrived yet.

Also reduces the update interval from 30s to 10s so relative timestamps
stay responsive.
2026-04-26 18:55:49 -04:00
Anso 819d2a63fc feat(stacks): add Schedule task shortcut to stack context and kebab menus (#772)
Right-clicking a stack or opening its 3-dot menu now shows a
'Schedule task' entry in the lifecycle group (visible to paid tiers).
Clicking it navigates to Scheduled Operations and opens the New
Schedule dialog pre-filled with the stack name and active node,
removing the need to navigate there manually and re-enter the target.

- Added openScheduleTask to StackMenuCtx; wired in buildMenuCtx using
  the active node from NodeContext
- Extended ScheduledOperationsView with optional prefill/onPrefillConsumed
  props; a ref-guarded effect calls openCreate() with the prefill data
- openCreate refactored to accept an optional prefill arg, removing the
  duplication between the effect and the existing 'New Schedule' button
2026-04-25 11:49:01 -04:00
Anso af9cb0aa63 feat(auto-update): per-stack auto-update enable/disable toggle (#771)
* feat(auto-update): add per-stack auto-update enable/disable toggle

Paid users (Skipper and Admiral) can now opt individual stacks out of
scheduled auto-updates from the stack context menu without disabling
the global feature.

- Add stack_auto_update_settings table (node_id, stack_name) with
  default enabled=true; four typed DatabaseService accessors with
  parameterized queries.
- Add GET /stacks/auto-update-settings, GET /stacks/:name/auto-update,
  and PUT /stacks/:name/auto-update (requirePaid + requireAdmin).
  PUT broadcasts state-invalidate with action auto-update-settings-changed
  so all open tabs refresh immediately.
- Stack DELETE clears the auto-update setting row alongside stack_update_status.
- autoUpdateRouter /execute skips disabled stacks before any registry
  call; skip is recorded in the results array. Manual Update actions
  are not affected.
- Add Auto-update: Enabled/Disabled toggle in the stack inspect group
  (paid tiers only, hidden for Community, consistent with Auto-Heal).
  Toggle uses optimistic update with revert-on-error toast.
- AutoUpdateReadinessView shows an Auto: Off pill and disables the
  Apply now button for stacks with auto-updates off. Detection still
  runs so the readiness card remains visible.
- Add 21 backend Vitest tests covering DB round-trips, endpoint auth
  and tier gates, execute skip for both wildcard and named targets.
  Add 3 frontend hook tests for toggle visibility and callback behavior.

* docs(auto-update): document per-stack auto-update control

Add a Per-stack control section to the auto-update readiness page
explaining how to disable and re-enable auto-updates for individual
stacks, what disabling means (scheduled apply skipped; detection still
runs; manual update unaffected), and a troubleshooting entry for
scheduled runs not applying to a specific stack.
2026-04-25 10:50:21 -04:00
Anso 1ef96582e1 feat(sidebar): keyboard shortcuts for stack menu actions (#729)
* feat(sidebar): implement keyboard shortcuts for stack menu actions

Shortcut labels shown in the context menu and kebab menu were purely
decorative. This wires them up to the corresponding actions on the
currently selected stack.

Cmd/Ctrl shortcuts: Enter (deploy), . (stop), R (restart), Up (update),
Backspace (delete). Single-key shortcuts: a (alerts), h (auto-heal,
paid), u (check updates), p (pin/unpin).

Guards: shortcuts are blocked when an input element is focused, when the
global command palette dialog is open, when no stack is selected, or
when the stack is busy. All visibility and busy flags from the menu
context are respected.

* docs(sidebar): document keyboard shortcuts for stack actions
2026-04-23 16:45:10 -04:00
Anso 856de35a52 feat(search): add global Ctrl+K command palette (#711)
* feat(search): add global command palette with cross-node stacks

Press Ctrl+K from anywhere to open a search palette that jumps to
pages, switches nodes, or opens stacks on any online node in the
fleet. Trigger lives as an icon in the top bar. Replaces the former
sidebar-scoped Ctrl+K handler.

The cross-node stack search fan-out is extracted into a shared hook
so the sidebar and palette stay in sync on the same debounce +
abort shape.

* fix(search): drop render-time ref write and effect-based query reset

Replace `nodesRef.current = nodes` during render with direct use of
`nodes` in the stack select callback, and fold the query-reset logic
into a single `onOpenChange` handler so it no longer runs via an
effect. Both changes resolve react-hooks rule violations that broke
frontend lint in CI.
2026-04-20 13:57:40 -04:00
Anso c7dfde4b79 fix(sidebar): distinct fuchsia notification dot for update indicator (#708)
The pulsing dot signaling an available image update used --info (blue
hue 250), the same hue as --label-blue, so the two blended visually
when a stack carried a blue label. Introduce a dedicated --update
token in vibrant fuchsia (hue 320) sitting outside the label palette,
and render both the sidebar StackRow and the fleet NodeManager
indicator as a notification-dot pattern (solid stationary core with
animate-ping halo). Aligns with the design rule that static state
should not pulse.
2026-04-20 11:24:41 -04:00
Anso 75370d8fce feat(sidebar): inline label create, live sync, and kebab submenu parity (#706)
Portal-wrap DropdownMenuSubContent so the kebab Labels submenu renders outside
the clipped dropdown container, matching ContextMenuSubContent and fixing the
empty/broken kebab submenu.

Thread onLabelsChanged from LabelsSection through SettingsModal to
EditorLayout so label creates and deletes in Settings propagate to the
sidebar menus without a page refresh.

Add an inline "New label" form in both the kebab and context menu label
submenus that creates and assigns a label in one interaction, removing the
Settings round-trip from the label assignment flow.
2026-04-20 09:57:20 -04:00
Anso 370b67d7ec feat(sidebar): cockpit redesign with grouped stacks and activity footer (#702)
* chore: ignore .superpowers/ brainstorm scratch dir

* feat(sidebar): add useStackMenuItems hook with grouped menu model

Pure transform hook that converts StackMenuCtx into four ordered MenuGroup
arrays (inspect, organize, lifecycle, destructive). Shared type contract in
sidebar-types.ts gives both the ContextMenu and DropdownMenu a single source
of truth so they cannot drift. Covered by 8 unit tests.

* refactor(sidebar): stabilize useStackMenuItems memoization deps

Destructure menuVisibility flags into primitive deps so inline object
literals from callers do not defeat memoization. Add a test confirming
isBusy disables all lifecycle items.

* feat(sidebar): add usePinnedStacks hook with per-node localStorage

* refactor(sidebar): stabilize usePinnedStacks eviction signal and isPinned dep

Change evictedOldest shape to { file, seq } so consumer effects re-fire on
repeated evictions. Narrow isPinned's useCallback dep to the current node's
pinned list so it only rebinds on local changes. Add test for eviction
side-effect and a second test for the seq counter.

* feat(sidebar): add useSidebarGroupCollapse hook with per-node keys

* refactor(sidebar): tighten useSidebarGroupCollapse effect ordering

Collapse the two write/read effects into a single skip-next-write ref
pattern so switching nodes no longer writes the previous node's map under
the new key before hydration. Also skip the no-op mount write. Add test
for setCollapsed.

* feat(sidebar): add row + group-header style helpers

* feat(sidebar): add SidebarBrand with mono kicker + serif hero

* feat(sidebar): add SidebarActions wrapper for create + scan

* feat(sidebar): extract SidebarSearch with kbd pill

* feat(sidebar): add StackRow with cyan-rail active state

* refactor(sidebar): dedupe tooltip markup in StackRow, widen test coverage

Extract a local RowTooltip helper so the update and git-pending branches
share the CursorProvider scaffolding. Add four behavioral tests covering
click, keyboard activation, kebab stop-propagation, and the busy loader
branch.

* feat(sidebar): unify context + kebab menus via useStackMenuItems

* feat(sidebar): add StackGroup with collapse and pinned variant

* feat(sidebar): add StackList with pinned + label groups

* feat(sidebar): add SidebarActivityTicker with idle fallback

* feat(sidebar): add StackSidebar container composing the regions

* feat(sidebar): replace sidebar block with StackSidebar composition

* docs(sidebar): add stack sidebar feature page with screenshots

* fix(sidebar): satisfy react-hooks purity and memoization rules

* fix(sidebar): restore "Sencho Logo" alt text for E2E selector
2026-04-19 21:45:01 -04:00