* 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.
* feat: pilot agent outbound-mode for remote nodes
Adds a second mode for managing remote nodes: the agent dials an outbound
WebSocket tunnel to the primary, so the remote host no longer needs an
inbound port, a reachable URL, or its own TLS certificate. Works behind
NAT, residential routers, and corporate firewalls.
The primary multiplexes HTTP and WebSocket requests over a single tunnel
via a hybrid JSON + binary frame protocol, bridged through a per-tunnel
loopback server so existing proxy and upgrade handlers route pilot-mode
nodes identically to proxy-mode ones.
Enrollment uses a single-use 15-minute pilot_enroll JWT exchanged for a
long-lived pilot_tunnel credential on first connect. Proxy mode continues
to work unchanged and both modes are supported side-by-side.
* test(e2e): switch to proxy mode before asserting api_url field
Remote nodes default to Pilot Agent mode, which hides the api_url input.
The SSRF-validation tests need proxy mode, so the helper now selects
Distributed API Proxy after picking Remote type before asserting the
field is visible.
* fix(e2e): wire Combobox id prop so node-mode selector resolves
The Combobox trigger button had no id, leaving its Label orphaned and
making getByRole name-based lookups fail. Adding id to the primitive,
passing id="node-mode" from NodeManager, and updating the E2E helper to
use #node-mode fixes both the a11y regression and the CI timeout.
* feat: add Custom OIDC provider and move SSO to Community tier
Add a generic Custom OIDC provider that works with any spec-compliant
OIDC identity provider (Keycloak, Authentik, Authelia, Zitadel, KanIDM,
Pocket ID, etc.) via standard discovery. Supports configurable claim
mapping for User ID, Username, and Email fields to handle non-standard
providers.
Move all SSO functionality (LDAP and OIDC) from the Admiral tier to the
Community tier so every user has access to identity provider integration.
Backend: add oidc_custom to AuthProvider type, extend SSOService with
claim mapping fields and env-var seeding, add oidc_custom to route
validation, remove requireAdmiral guards from SSO config endpoints.
Frontend: add Custom OIDC card with Display Name, Issuer URL, and claim
mapping fields to SSOSection; add KeyRound icon on login page; remove
AdmiralGate wrapper and lock icon from SSO settings nav.
Tests: update tier guard expectations, add oidc_custom authorize/config/
provisioning tests and claim mapping coverage. All 992 tests pass.
Docs: add Custom OIDC configuration reference, provider-specific setup
examples, troubleshooting section, and updated screenshots.
* fix: settings dialog close button overlap and combobox styling
Reposition the close button in Settings Hub above the scroll area so
it stays fixed when content scrolls. Increase dialog height to
accommodate the growing number of setting sections.
Fix combobox trigger styling to match Input component tokens
(border-glass-border, bg-input) and eliminate the gap between trigger
and dropdown list (top-full -mt-px). Apply the same fixes to
multi-select-combobox for consistency.
Add items-start to the Scopes/Default Role grid so the combobox
aligns with the adjacent input field. Add showClose prop to
DialogContent for consumers that need custom close button placement.
Update SSO doc screenshots at 1920x900.
* fix(audit): harden audit log with summary fixes, design compliance, and test coverage
Replace stale /compose/* route summaries with current /stacks/* patterns,
add ~30 missing route summaries for container ops, labels, fleet updates,
SSO, templates, and auto-update. Fix X-Forwarded-For extracting full
proxy chain instead of first client IP. Pre-sort pattern table at module
load for O(1) lookup instead of sorting per request.
Add Calendar and DatePicker UI components (react-day-picker v9 + date-fns)
replacing browser-native date inputs. Align AuditLogView with design system:
Combobox instead of Select, design token colors, strokeWidth 1.5, card bevel,
tabular-nums on all numeric cells.
Add diagnostic logging gated behind developer mode for audit middleware,
query, and export endpoints. Extract getAuditSummary to testable utility.
Add 32 tests covering summary resolution, DB round-trips, API permissions,
export formats, and middleware integration.
* fix(audit): use correct Chevron prop types for react-day-picker compatibility
* 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.
Replace bg-card/80 with bg-popover/95 and match the backdrop-blur/saturate
values used by dialogs, dropdowns, and popovers. Remove per-type colored
gradient overlays so color differentiation comes from icons only. Unify the
determinate progress bar gradient with the loading bar's brand gradient.
Show a loading notification with spinner and indeterminate progress bar
while Resource Hub operations are in progress, replacing the dead moment
between confirmation and result.
* feat(labels): add stack_labels schema and DatabaseService CRUD methods
* feat(labels): add label CRUD, assignment, and bulk action API routes
* feat(labels): add oklch label color palette for light and dark themes
* feat(labels): add LabelPill and LabelDot reusable components
* feat(labels): add LabelAssignPopover component for inline label management
* feat(labels): add label pill bar, label dots, and label assignment to sidebar
* feat(labels): add label filtering and label dots to fleet view
* feat(labels): add label-scoped bulk actions (deploy/stop/restart all)
* docs: add Stack Labels feature documentation
* fix(labels): use context menu sub-menu for label assignment and add settings integration
Replace broken Popover-inside-ContextMenu pattern with native Radix
ContextMenuSub for reliable label toggling on right-click. Wrap
ContextMenuSubContent in a Portal to prevent overflow clipping. Add
"Manage labels..." item that opens Settings directly to Labels section.
Fix close button overlap in LabelsSection header. Add LabelsSection
settings component with full CRUD, assignment counts, and ProGate.
Add initialSection prop to SettingsModal for deep-linking. Include
screenshots for documentation.
* docs: update stack labels documentation with screenshots and corrected instructions
* fix(labels): address security and quality issues from code review
- Add NaN validation on parseInt(req.params.id) in label routes
- Scope updateLabel/deleteLabel by nodeId to prevent cross-node IDOR
- Validate labelIds belong to correct node in setStackLabels
- Add requireAdmin check on bulk action endpoint
- Replace error: any with error: unknown and proper narrowing
- Remove unused Label import from index.ts
- Remove unused isPro prop from LabelsSection
- Add strokeWidth={1.5} to Check icons per design system
* chore: update CHANGELOG with stack labels feature
* feat(auto-update): add auto-update policies and fix image update detection
Auto-Update Policies (Skipper+ tier):
- New scheduled task action type 'update' for check-then-update flow
- Dedicated AutoUpdatePoliciesView with CRUD, cron presets, and run history
- Conditional tier gating: Skipper gets auto-update, Admiral gets full scheduled ops
- Backend executeUpdate: checks digests, pulls only if newer, atomic redeploy
Image Update Detection fixes (all tiers):
- Fix stack name key mismatch: use working_dir label instead of project label
- Add 5-minute periodic frontend polling for background check results
- Replace fixed 3s timeout with polling-based manual refresh via /api/image-updates/status
- Clear update status after successful stack update
* fix(ui): remove Skipper tier badge from Auto-Update Policies header
* fix(ui): remove auto-update action from Scheduled Operations view
Admiral users have a dedicated Auto-Update view — showing update tasks
in Scheduled Operations too was confusing duplication. Each view now
owns a distinct, non-overlapping set of action types.
* fix(auto-update): fix node-stack linking and add All Stacks option
- Stack dropdown now re-fetches when node selection changes using
fetchForNode, and resets the selected stack
- Node selector moved above stack selector with stack disabled until
a node is picked
- Added "All Stacks" wildcard option that checks and updates every
stack on the selected node
- Backend executeUpdate refactored to iterate over all stacks when
target_id is "*", with per-stack error isolation
* refactor(ui): replace Select dropdowns with searchable Combobox component
Add a reusable Combobox component with inline search and use it for
Node/Stack selectors in both Auto-Update Policies and Scheduled
Operations dialogs. Also fixes node-stack linking bug where changing
node didn't update the stack list.
* fix(ui): resolve CI TypeScript errors in Combobox and ScheduledOperationsView
Add missing searchPlaceholder prop to ComboboxProps interface and remove
dead 'update' action filter that conflicted with the narrowed type union.
* fix(ui): use Geist Sans font in toast component
The toast renders via React portal on document.body, bypassing the app's
font inheritance. Add explicit font-family declaration using var(--font-sans)
to match Sencho's design system.
* refactor(ui): replace Sonner with custom Sera UI-inspired toast system
Replace the Sonner toast library with a custom implementation inspired by
Sera UI's glassmorphism design. The new system uses an external store pattern
with useSyncExternalStore for React integration, keeping the same
toast.success()/error()/warning() API so all 172 call sites required only
an import path change.
Key changes:
- New toast-store.ts: singleton store with identical API to Sonner
- New toast.tsx: Sera UI-faithful Notification component with Framer Motion
animations, frosted glass (backdrop-blur-xl), type gradient overlays,
animated progress bar (green→blue→sky gradient), and hover:scale-105
- Removed sonner and next-themes dependencies
- Rewired all 19 consumer files to import from the new store
* fix(ui): resolve ESLint errors in toast system
- Use const for listeners Set (prefer-const)
- Initialize startRef with 0 instead of Date.now() to satisfy
react-hooks/purity rule, set actual value inside useEffect
* feat(ui): add glassmorphism design tokens and utility classes
Introduce glass design system foundation: translucent oklch color
variables for both light and dark themes, glass/glass-border/glass-highlight
tokens, semantic status colors (success/warning/info), .glass and
.glass-strong utility classes with backdrop-filter, reduced shadow
values, and standardized spring animation presets in lib/motion.ts.
* feat(ui): apply glass treatment to core components
Update card, dialog, input, button, popover, sheet, tooltip,
dropdown-menu, context-menu, select, alert-dialog, and tabs
components with glassmorphism styling: translucent backgrounds via
new CSS variables, backdrop-blur layers, glass-border luminous
edges, and glass-highlight hover states.
* refactor(settings): decompose Settings Modal into section components
Extract 10 inline sections from the 1,987-line SettingsModal into
dedicated files under components/settings/. Introduce section
registry pattern replacing 14 conditional blocks. Add shared types,
sidebar navigation grouping with separators, glass treatment on
sidebar and nav buttons, and responsive modal height. SettingsModal
shell shrinks to ~380 lines.
* refactor(ui): unify all tabs to animate-ui TabsHighlight with glass styling
Migrate 4 tab instances (EditorLayout, FleetView, ResourcesView,
NotificationsSection) from inconsistent patterns (manual layoutId,
underline border-b-2, default fade) to the shared TabsHighlight
primitive with glass-highlight indicator and springs.snappy
transition. Standardize EditorLayout nav highlight spring config,
apply glass-highlight to sidebar stack list hover/active states,
and update mobile nav styling.
* refactor(ui): migrate hardcoded colors to semantic CSS variables
Replace hardcoded Tailwind color classes across ~19 component files
with semantic CSS variable classes: emerald/green to success,
orange/amber to warning, blue to info. Preserves brand/decorative
colors (Crown amber, Admiral blue). Enables consistent theming of
status indicators across the entire application.
* refactor(ui): Linear dark precision aesthetic — solid surfaces, depth cues, text hierarchy
Replace glassmorphism with Linear.app-inspired design: solid surface tokens
(card #111111, sidebar #0d0d0d, root #0a0a0a), backdrop-blur restricted to
floating overlays only (blur(10px) saturate(1.15)), desaturated teal accent,
font-weight 500 everywhere, monochrome chart palette, and three depth cues:
root ambient glow, luminous card top-edge, steep text brightness ramp.
* refactor(ui): precision polish — fix muddy dark, snowblind light, add design anchors
- Replace 34 hardcoded rgba values with theme-aware stat-* CSS tokens
- Fix light theme: solid white cards, off-white background, readable text
- Add card-border tokens with sharper directional lighting (top edge 2x)
- Add chart-grid/chart-tick tokens for theme-aware axis rendering
- Upgrade body glow: teal-tinted (dark), warm amber (light)
- Terminal-inspired sidebar: monospaced UP/DN status codes, Geist Mono
- Add tabular-nums to stat values to prevent layout jitter
- Light mode cards get shadow-sm for depth against off-white background
* refactor(ui): Linear materiality pass — ghosted nav, translucent sidebar, font unity
- De-escalate Delete button from solid destructive to ghost with hover fill
- Make sidebar translucent (80% opacity + backdrop-blur) so body glow bleeds through
- Bump dark nav accent to 0.07 for ghosted backlit selection
- Unify all terminal/editor fonts to Geist Mono (was JetBrains/Consolas mix)
- Add Monaco editor fontFamily for YAML/env editing consistency
- Add threshold-based color to Host RAM and Host Disk stat values (warn/crit)
* refactor(ui): material simulation — inherent depth, layer separation, recessed terminal
- Bump dark background 0.065→0.08, card surfaces 0.10→0.12 for 4% layer separation
- Add card-bevel token (inset top shimmer) for permanent structural depth
- Add button-inner-glow token for physical key feel on outline buttons
- Recess terminal with inset shadow and dimmed label
- Reduce action icon strokeWidth to 1.5 for refined industrial feel
- Add teal LED backlight bar on active nav item via blur pseudo-element
* fix(ui): parse usagePercent string to number for getValueColor
usagePercent is typed as string in SystemStats but getValueColor
expects number, causing TS2345 in CI builds.
* chore: remove CLAUDE.md and MANUAL_STEPS.md from git tracking
These files contain internal project instructions that should not be
public. Both are now in .gitignore to prevent re-tracking.
* chore: remove build output and generated files from git tracking
Remove backend/public/ (frontend build artifacts) and backend/cookies.txt
from tracking. Add demo-video/ and website/ to .gitignore.
* docs: align mintlify theme with website and app branding
Update colors to brand cyan, add Geist font, dark mode default,
gradient background, navbar with GitHub link and CTA, and footer
matching the website structure.
* chore: add shadcn MCP server configuration
* feat: add stack context menu, tier icons, centered logo, and support section
- Add right-click context menu on sidebar stacks with full actions
(alerts, check updates, deploy, stop, restart, update, delete)
- Mirror same actions in the three-dot dropdown menu
- Create TierBadge component with Globe/Crown/Users icons for
Community/Pro/Team tiers, replacing ProBadge across the app
- Center logo and app name in sidebar header
- Add tiered Support section in Settings with docs, GitHub issues,
and tier-appropriate email support channels
- Remove duplicate links from About section
* docs: update changelog and stack management docs for context menu
Add minHeight={0} alongside minWidth={0} on ResponsiveContainer and
remove default aspect-video class that conflicts with explicit h-full
sizing, causing -1 dimension measurements during initial layout.
Add https: to img-src CSP directive so App Store template icons from
external registries (raw.githubusercontent.com) load correctly. Disable
Origin-Agent-Cluster header (only meaningful over HTTPS). Add minWidth={0}
to Recharts ResponsiveContainer to suppress dimension warnings.
* chore(deps): upgrade ESLint 9 → 10 with plugin compatibility fixes
- eslint: ^9 → ^10.0.0 (backend + frontend)
- @eslint/js: ^9 → ^10.0.0 (backend + frontend)
- eslint-plugin-react-refresh: ^0.4.24 → ^0.5.2 (ESM, config factory API)
- Update frontend eslint.config.js: destructured import for react-refresh,
call configs.vite() as factory function (0.5 API change)
- Downgrade new ESLint 10 rules (no-useless-assignment, preserve-caught-error)
to warnings for existing code patterns
- eslint-plugin-react-hooks stays at 7.0.1 (stable) with --legacy-peer-deps
until a stable release adds ESLint 10 peer support
* chore(deps): upgrade recharts 2.x to 3.8 with chart.tsx type fixes
recharts 3.x moved Tooltip/Legend props to context-based API.
Updated chart.tsx to use explicit prop interfaces with internal
recharts type imports (LegendPayload, TooltipPayload, TooltipFormatter).
* chore(deps): upgrade TypeScript 5.9 to 6.0
- Remove deprecated baseUrl from frontend tsconfig (paths works standalone in TS 6)
- Add react-is dependency required by recharts 3.x at build time
- Backend and frontend both compile and lint cleanly
* chore(deps): upgrade Vite 7.3 to 8.0 and @vitejs/plugin-react to 6.0
Vite 8 replaces Rollup+esbuild with Rolldown, significantly improving
build speed (~2s vs ~18s). No config changes required.
* fix(ci): add .npmrc with legacy-peer-deps for CI and Docker builds
typescript-eslint@8.x requires typescript <6.0.0 and
eslint-plugin-react-hooks@7.0.1 requires eslint <=9. Until upstream
packages release compatible versions, legacy-peer-deps is needed.
* docs: add logo assets and re-ignore CLAUDE.md
* fix(ci): copy .npmrc into prod-deps Docker stage
The prod-deps stage also runs npm ci with backend/package.json but was
missing the .npmrc needed to bypass peer dep conflicts.
- Replace catch (error: any) with catch (error) + (error as Error).message cast
in EditorLayout, NodeManager, HomeDashboard, AppStoreView
- Define TemplateVolume interface in AppStoreView; replace volumes any[] with typed array
- Define MetricPoint interface in HomeDashboard; replace metrics any[] with MetricPoint[]
- Define TerminalContainer type in BashExecModal; replace as any DOM property casts
- Define NodeTestInfo interface in NodeManager; replace info: any with typed shape
- Fix DockerNetworkStats cast in EditorLayout container stats WebSocket handler
- Remove unused catch variable (e) in api.ts and other components
- Cast streamFilter onValueChange val to union type in GlobalObservabilityView
- Add eslint-disable-next-line react-refresh/only-export-components to badge.tsx,
button.tsx, AuthContext, NodeContext, use-data-state, use-is-in-view
- Add eslint-disable-next-line react-hooks/set-state-in-effect in LogViewer
- Add /* eslint-disable */ to animate-ui third-party primitive files
- Fix rate limiter to allow 100 attempts in dev mode so E2E tests are
not blocked by failed-login attempts during test development
- Simplify logout button selector to use Lucide icon class (lucide-log-out)
instead of the fragile Tooltip-content locator chain that broke on navigation
- Rewrite stacks E2E spec: use waitForFunction to wait for sidebar to load,
delete leftover stacks via browser-context fetch before creating, and use
page.reload() to get a clean sidebar state
- Fix AlertDialogContent: remove asChild+motion.div pattern that triggered a
React.Children.only crash — Radix AlertDialog.Content injects a second
DescriptionWarning child internally, breaking Slot when asChild=true; replace
with CSS keyframe animations (data-[state=open]:animate-in)
- Fix final assertion in delete test to use exact text + listbox scope to avoid
false positives from similarly-named stacks like e2e-test-stack-*
- All 6 E2E tests pass (4 auth + 2 stacks); node tests skip gracefully
- fix(editor): Monaco compose↔.env tab switching no longer accumulates height
each switch — root cause was Monaco's position:static internal child creating
a circular CSS dependency with the CSS grid; broken by calling layout({0,0})
to force reflow before re-measuring. Added overflow-hidden to Monaco container.
- fix(ui): sliding tab indicator on compose/env tabs via motion.div layoutId
- fix(ui): sliding tab indicator on Notification tabs (Discord/Slack/Webhook)
- fix(ui): switch thumb now animates position (Radix data-attribute CSS translation)
- fix(ui): 'Always Local' tooltip no longer crashes — replaced animate-ui tooltip
(getStrictContext throws outside provider) with pure Radix primitives + CSS animation
- feat(settings): Auto theme option added (light/dark/auto with matchMedia listener)
- fix(ui): NodeManager dialog buttons no longer stuck together (DialogFooter gap)
- fix(ui): AlertDialog spring animation + Quick Clean button text wraps correctly
- fix(ui): Resources/App Store/Logs buttons toggle off on second click
Install motion + animate-ui, overhaul design tokens with brand cyan accent,
and replace CSS keyframe animations in Dialog, Tabs, Switch, and Tooltip
with spring-physics and blur-fade transitions via animate-ui Radix primitives.
- Implemented ResourcesView component to display Docker resource usage.
- Integrated recharts for visual representation of reclaimable resources.
- Added functionality to fetch and manage Docker images, volumes, networks, and orphan containers.
- Created reusable table components for displaying resource data.
- Included confirmation dialogs for prune and delete actions.
- Enhanced UI with Tailwind CSS for better user experience.