* feat: add sidebar update indicator toggle and Stack Health badge
- Add image_update_sidebar_indicators setting (default off, node-scoped)
- Gate the Updates filter chip and sidebar status indicators on the setting
- Add "Update available" badge to Stack Health table (always visible)
- Extend ImageUpdateStatus with sidebarIndicators boolean
- Poll /api/image-updates/status alongside /detail in useImageUpdates
- React to SENCHO_SETTINGS_CHANGED for instant toggle propagation
- Reset sidebar state on node switch; generation-guard stale responses
- Disable toggle when status is null (loading) or field is absent (old node)
- Wire stackUpdates through ViewRouter → HomeDashboard → StackHealthTable
- Update settings registry, operator docs, and sidebar/dashboard docs
* fix: guard against stale node renders, memo drift, and cross-node error toasts
- Track owning node ID in useImageUpdates state so React never renders
node B with node A's data before the passive effect resets (P2)
- Replace incorrect stackUpdates dependency with sidebarStackUpdates in
chipFilteredFiles useMemo (P3)
- Guard the error toast in handleSidebarIndicatorsChange so a stale PATCH
failure from node A does not surface while viewing node B (P3)
* fix: default sidebar update indicators to on (opt-out)
The sidebar indicators are a safe convenience that most users want.
Switching the default from off to on matches the opt-out convention
used by prune_on_update, reclaim_hero, and health_gate_enabled.
Make the Resources Images, Volumes, and Networks tables sortable with a
shared useTableSort hook and SortableTableHead, move the Images scan-history
control into the Images tab header, and keep the network List/Topology toggle
anchored with Create Network visible in both modes.
Rework the dashboard Stack health table: drop the redundant Host column, add
sortable Stack/Up/CPU/Mem headers, and add Source (local/git) and Port columns.
The status endpoint now labels each stack with its git/local source, computed
outside the cache so linking changes show immediately.
Extract a reusable CreateNetworkDialog and add a create-network action to the
stack-detail Networking tab.
Bulk stack-status aggregation collapsed a stack to "running" as soon as any
container was up, so a multi-container stack with crashed containers showed a
green UP pill and the degradation was invisible from the sidebar.
Add a crash-aware "partial" state: a stack is partial when at least one
container is running and at least one has genuinely failed (exited with a
non-zero code, dead, or crash-looping). Cleanly finished one-shot containers
(exit 0) and clean restart-policy cycling do not count, so an app with a
completed init job stays UP. The exit code is read from the container Status
string, so no extra inspect calls are needed.
Render partial as an amber PT pill with a hover tooltip showing the
running/total count, fold it into the Down filter (needs-attention), and treat
it as running for context-menu lifecycle actions so operators keep
stop/restart/update. The dashboard stack-health table, cross-node search rows,
and the command palette all pick up the new state through the shared status
surfaces.
* feat(appearance): add Calm/Signature visual style, readability mode, and chart palette
Turn the "too intense / italic headers hurt / the security graph fights my
eyes" feedback into a token-driven Visual style with Calm as the new default
and Signature one click back to the prior look.
- Heading family routes through a `.font-heading` utility driven by
`--font-heading`/`--heading-style`: operational headings render upright in the
interface face under Calm and italic Instrument Serif under Signature. Base
rule sets family + style only, so each call site keeps its own weight/tracking
and Signature stays a true no-op; the Calm lift is a `[data-headings="clean"]`
descendant rule. Brand lockup, empty-state heroes, and onboarding stay serif.
- Severity charts resolve through `--sev-*` tokens with Muted, Heat, and
Signature palettes; FindingsByType routes its series through the severity ramp
plus a neutral so no brand-cyan sits next to rose. The risk trend flattens its
gradient under Muted/Heat/reduced and keeps the gradient under Signature.
- Appearance settings gain Visual style cards, a Security visualization palette,
a Readability master toggle, a Motion & effects group, and a "Reset to default"
button (restores the Calm axes, disabled while readability is on). Contrast
moves under Readability and Ambient glow under Motion & effects. A card is
selected only while the stored sub-axes match its preset, so a custom
combination de-selects both.
- The topbar Theme quick-switch swaps the interface/data font pickers for a
Visual style switch and a Readability toggle (text size kept); its footer
Settings link jumps straight to Appearance.
- Readability is a sticky master that forces the calm resolution and a contrast
lift at apply time without mutating the stored sub-axes.
- New users default to Calm; any pre-existing persisted appearance state keeps
the Signature look. The pre-paint script mirrors the store.
- SegmentedControl gains a `disabled` prop and a nullable value (no active
segment for a custom combination, with a roving-tabindex keyboard anchor).
Adds unit/component coverage for the store, migration, chart shape logic, the
disabled control, the reset/de-selection, and the quick-switch.
* fix(appearance): migrate Blueprint serif headings and surface readability locks
- Migrate the two operational Blueprint headings (catalog tile name, drift-policy
option title) from font-serif italic to the .font-heading utility; the first
pass only covered font-display, so Calm still left these italic. font-serif and
font-display both resolve to the same display face, so this is the same fix.
- Lock the Visual style cards under Readability (parity with the topbar switch and
the on-screen guidance to turn Readability off to choose a style by hand).
- Lock the Border brightness slider under Readability and show its forced +0.03
readout, since Readability overrides the stored value; dragging it previously
appeared to do nothing.
- Correct the Appearance docs sentence for the topbar quick switch (it listed
fonts; the quick switch now carries visual style, readability, and text size).
The dashboard Stack health UP column counted from each container's
Created timestamp, which never moves on stop/start or restart, so a
restarted container kept reporting its original age. Resolve uptime from
State.StartedAt (via a briefly cached inspect with bounded concurrency,
falling back to Created when inspect is unavailable) so it reflects the
real time since last start.
The current CPU and MEM columns separately summed the latest sample per
container with no recency filter, letting a recently stopped container's
final reading linger in the totals. Drop samples that trail the freshest
sample by more than the stale window so stopped containers leave the sum.
* feat(ui): make Fleet, Settings, and the dashboard table usable on mobile
Tier 2 of the mobile pass, all gated below the md breakpoint so desktop
renders identically:
- Fleet: the tab strip scrolls horizontally and the action row (Check
Updates / Refresh / Add Node) wraps instead of clipping.
- Settings: below md the nav rail is a full-screen list; choosing a section
pushes it full-screen with a back affordance (master/detail), matching the
stack flow. Desktop keeps the two-pane layout.
- Dashboard: the fixed stack-health table scrolls horizontally on a phone.
Adds a Playwright desktop visual-regression spec (1280/1440/1920) as a
zero-desktop-change gate; its environment-specific snapshots are gitignored.
* test(ui): harden the desktop visual-regression gate
Mask only the live sidebar ticker and notification count instead of the whole
sidebar / top-bar shell, so the gate now proves that shell unchanged too.
Lower the pixel budget to 1200 (the only residual churn is in-content live
stats); run against seeded / frozen data in CI for a zero-tolerance gate. Adds
a data-testid to the activity ticker so it can be masked precisely.
Adds a per-device appearance preference that compresses row, cell, and
tile padding across dashboard, settings, audit log, and every shared
table without changing typography or layout structure.
Density is stored in localStorage and applied to the document body as a
class that swaps a set of CSS variables. Components opt in by consuming
the tokens, so the global table primitive scales all seven consumers at
once.
A new Appearance section in the Identity group lets users pick between
Comfortable and Compact via a Combobox, with a helper line that
reflects the current choice.
* feat(dashboard): status masthead, unified gauges, stack health sparklines
Rework the home dashboard around a single cyan-railed status masthead
that carries the health state word, node meta, and reasons inline. The
resource block collapses into one strip with a CPU hero sparkline,
memory and disk gauge bars, and a network tile whose sparkline is built
from per-container byte-counter deltas. Stack health becomes an
8-column mono table with row tinting, an uptime column sourced from
the oldest running container's creation time, and a per-stack 10-minute
CPU sparkline. Historical charts pick up a cyan gradient and an amber
peak marker. A shared Sparkline primitive backs all of the above.
* docs(dashboard): refresh screenshot for phase B layout
* fix(dashboard): anchor sparkline bucketing to latest metric timestamp
The cpuHistory, netHistory, and cpuPeakLabel memos called Date.now()
inside useMemo, which violates react-hooks/purity: the rule fires
because re-renders can produce different bucket boundaries from the
same inputs. Derive a historyEndAt anchor from the newest metric
sample in the polled series and thread it through to ResourceGauges.
* fix(dashboard): harden real-time dashboard with bug fixes and design compliance
- Fix host alert spam: add 5-minute cooldown for CPU/RAM/disk threshold
alerts, preventing duplicate notifications every 30s during sustained
breaches. Extract shared dispatchWithCooldown helper (also used by
Docker janitor alerts).
- Fix memory metric inflation: subtract filesystem cache from stored
memory_mb values, matching the existing calculateMemoryPercent logic.
- Fix crash detection reliability: replace fragile 'seconds ago' string
matching with a tracked Set of alerted container IDs. Containers are
only alerted once per crash event, with automatic cleanup when they
start running again or after a 1-hour TTL.
- Fix health status bar: exited containers now trigger 'degraded' state
independently of unread error notifications.
- Fix CPU chart Y-axis: auto-scale when aggregate container CPU exceeds
100% instead of silently clipping at the hardcoded domain ceiling.
- Fix grammar: 'actives' to 'active' in container count label.
- Add shadow-card-bevel to all dashboard cards per design system.
- Update dashboard docs to reflect revised health status thresholds.
* test(dashboard): update monitor service tests for new alert signatures
- Add container Id fields to crash detection test fixtures
- Update host alert assertions to match dispatchWithCooldown 3-arg call
- Fix unhealthy container test to use State: 'unhealthy' instead of
State: 'running' (running containers are now skipped in crash detect)
* feat(dashboard): drop CPU column and relative timestamp from Stack Health and status bar
The Stack Health table's CPU column duplicated data already surfaced in
the top ResourceGauges and the CPU Usage historical chart. The health
status bar's 'just now' timestamp was cosmetic: no consumer relied on
lastUpdated state for polling, staleness detection, or conditional
rendering. Removing both tightens the dashboard and eliminates a dead
prop chain through useDashboardData.
* refactor: remove dead admin_email field from setup flow
The Setup form captured an admin email under 'Used for license recovery.
Never shared with third parties.' but the value was written to
global_settings and read nowhere: no license recovery, SMTP, or support
contact flow consumed it. Rather than building UI on top of the dead
field, delete the input, the payload key, and the backend persistence.
Any orphaned row from prior setups is harmless and the frontend ignores
unknown settings keys.
* feat(settings): use Radix ScrollArea with per-section scroll memory
Settings Hub used a native-scroll div that snapped to the top every time
the user switched subsections and exposed the default browser scrollbar.
Wrap the nav and content panes with the shadcn ScrollArea (Radix under
the hood, type='hover') and expose a viewportRef so the modal can stash
each section's scrollTop in a ref and restore it via useLayoutEffect on
switch. Style the thumb with translucent foreground tokens so it reads
as glass against popovers and dialogs. Replaces a hand-rolled scroll
hook and ad-hoc CSS utility.
* feat(dashboard): redesign as DevOps command center
Transform the dashboard from a basic stats viewer into a high-signal
operational command center with 5 composable sections:
- Health status bar with system health derivation (Healthy/Degraded/Critical)
- Resource gauges with visual progress bars and threshold coloring
- Paginated stack health table with per-stack UP/DN, CPU, memory, and
click-to-navigate (8 per page)
- Enhanced historical CPU/RAM charts with skeleton empty states
- Recent alerts feed with severity-coded notifications
Extract monolithic HomeDashboard.tsx (447 lines) into composable
sub-components under dashboard/ directory. Remove Docker Run to Compose
converter from the landing surface. Add defensive .ok check on container
status fallback in EditorLayout.
* feat(dashboard): add Clear All Notifications button to Recent Alerts
Add a destructive ghost button below the alerts feed that calls
DELETE /api/notifications to clear all notifications, then refreshes
the list. Button only appears when there are alerts to clear.
* feat(dashboard): add pagination to Recent Alerts section
Same pattern as Stack Health table: 8 items per page with prev/next
chevron controls and page indicator in the card header. Pagination
auto-hides when there are 8 or fewer alerts. Page resets on clear all.
* fix(dashboard): resolve container count oscillation and add cursor hover detail
Fix container stats flickering between 0 and correct values by moving
state resets to the top of each useEffect body (runs once per node
switch, not on every poll tick). Add animate-ui cursor primitive and
wire it to the active containers number in ResourceGauges to show
managed/external breakdown on hover. Silence noisy Docker socket
errors when engine is unreachable.
* feat(dashboard): add cursor hover to health status with reason breakdown
Wrap the health badge (pulsing dot + label) in a CursorFollow tooltip
that explains why the node is Critical, Degraded, or Healthy. Shows
specific metrics (e.g. "RAM at 97.9%", "Disk at 96.4%") when hovered.
Displays "All systems nominal" for healthy nodes.
* fix(dashboard): resolve OOM from unbounded Docker stats polling
Three root causes addressed:
1. updateGlobalDockerNetwork had no overlap guard. When Docker was slow,
3-second interval ticks stacked up, creating dozens of concurrent
container.stats() calls that exhausted the heap. Added isUpdatingNetwork
flag and increased interval from 3s to 5s.
2. Historical metrics query returned ~20K rows (1-minute buckets x 14
containers x 24h). Downsampled to 5-minute buckets, reducing response
size by ~5x.
3. Dashboard polling continued when the browser tab was hidden, creating
phantom load. Replaced setInterval with visibilityInterval helper that
pauses polling on tab hide and resumes with an immediate fetch on focus.
* fix(dashboard): use loadFile for stack navigation from Stack Health table
The onNavigateToStack callback was only calling setSelectedFile and
setActiveView, skipping the full load flow (YAML content, env files,
containers, backup info). This caused the editor to show stale state
with a "Start" button for running stacks and empty YAML. Now calls
loadFile() which is the same path the sidebar uses.
* refactor(dashboard): simplify Containers card layout
Replace 2-column grid with vertical layout matching other gauge cards.
Active count uses text-2xl hero number, exited count sits in subtitle
position. Removed redundant total count row.
* fix(dashboard): unify notification types and fix multi-node clear
- Replace duplicate Notification interface in EditorLayout with shared
NotificationItem from dashboard/types.ts
- Tighten is_read type from number | boolean to number (matches SQLite)
- Pass notifications from EditorLayout (which aggregates all nodes) to
HomeDashboard as props, removing duplicate local-only polling from
useDashboardData
- Fix Clear All to use clearAllNotifications (deletes from all nodes)
instead of fetchNotifications (which was just a re-fetch, causing
remote notifications to reappear immediately after clearing)
- Delegate DELETE responsibility from RecentAlerts to parent handler
* fix(dashboard): handle optional nodeId in notification operations
Guard against undefined nodeId when calling fetchForNode for mark-read,
delete, and clear-all notification operations. The shared NotificationItem
type has nodeId as optional since the API response doesn't include it;
EditorLayout enriches it but TypeScript correctly flags the possibility.