Remove redundant nav and label dots, move desktop masthead status to animated left rails, and let AuthCanvas scroll long preflight content on small viewports.
A batch of UI/UX polish:
- New independent "Reduced motion" appearance setting (separate from Reduced
effects). Drives framer-motion via MotionConfig and clamps CSS transitions via
data-motion on <html>; toasts are unaffected. Defaults off (OS preference
still honored).
- Stack-detail Files tab: rename "Files & Volumes" to "Files", add a persisted
word-wrap toggle to the file viewer (default on), and add a fullscreen toggle
that collapses the Command Center + Logs column so the editor fills the width.
- Create Stack > From Git: remove the nested scroll clamp so the deploy toggle
and footer are reachable.
- Fleet: full-width tab band with icon-only Refresh / Export Dossier, icon-only
Check-for-updates / Add-node on the Overview toolbar, theme-aware empty-state
headings (calm drops the italic), and fix the Actions card body overlapping
the action-row divider.
- Snapshots: restyle Restore and Restore all to the ghost button design used by
View / Preview / Download, and right-align the per-stack Restore.
- Settings sidebar: App Store gradient active style and standard font size.
- Compose Doctor: dismiss the high-risk banner (and clear the tab dot) until the
findings change, via a shared fingerprint-keyed hook.
- Stack-detail Storage: link the "no recent fleet snapshot" warning to the Fleet
Snapshots tab (FleetView tabs are now controlled to support the deep link).
* 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).
* feat(ui): make the core stack flow usable on mobile
Below the md breakpoint the app collapses to a single full-width column:
the stack list is full-screen, tapping a stack opens a full-screen detail
with a Health / Logs / Compose segmented control (Logs first) and a back
button, and a bottom tab bar switches Stacks, Fleet, Schedules, and
Settings. Compose is read-only on a phone with a prompt to edit on desktop.
Desktop (md and up) is unchanged: the mobile shell is gated behind a
useIsMobile hook plus max-md/md variants, and the stack-detail blocks are
shared with the desktop two-pane view so it renders identically.
Also generalizes the unsaved-changes guard so leaving a dirty editor (back,
tab bar, hamburger) prompts before discarding; adds 44px touch targets on
list rows, filter chips, and actions; makes log and shell modals full-screen
on mobile; and offsets toasts and the deploy pill above the bottom tab bar.
* fix(ui): keep mobile nav in sync when opening views from outside the bottom bar
On a phone the sidebar activity actions, the node switcher's Manage Nodes, the
profile Settings entry, and the dashboard configuration links set the active
view without flipping the mobile surface to content, so the user stayed on the
stack list and never saw the destination. Route these through the mobile-aware
navigation and settings helpers (a no-op on desktop).
* fix(theme): make sheets and dialogs track the active theme
Portaled panels (scan-history and other sheets, dialogs, confirm modals)
only partially followed the theme: their popover surface was a flat per-theme
literal, so OLED mode never reached true black inside them and the contrast
control did not move their fill. The accent stopped at the thin rail because
the ambient glow was painted only on the main app shell, never inside a
portaled panel, and the overlay scrim was a hardcoded black.
- Derive the popover surface from per-theme lightness and alpha plus the
contrast spread, the same way the page background is derived, so every
floating surface tones with the active theme (including OLED true black) and
deepens with contrast. Dim and Light are unchanged at the default contrast.
- Add an accent wash to sheets and large dialogs so the chosen accent reads
inside them, matching the page. Small dropdowns and tooltips keep the themed
tone without the wash.
- Replace the hardcoded modal and sheet scrim with a theme-aware token that
softens to a light dim in the Light theme.
Tested across Dim, OLED, and Light with multiple accents and contrast levels.
* fix(theme): keep the command palette token-only (no accent wash)
The command palette renders through DialogContent, so it picked up the accent
wash meant for content sheets and dialogs. It is a command menu, so it should
stay token-only like dropdowns and tooltips. Add an optional panelGlow flag to
DialogContent (on by default, so every other dialog is unchanged) and turn it
off for the palette.
* feat(appearance): add theme, accent, contrast, and typography personalization
Expand Settings to Appearance into a full personalization surface and add a
quick switcher to the top bar (the palette button between search and
notifications). Choices are saved to the browser, sync across tabs, and apply
before first paint so there is no flash on reload.
- Themes: Dim (the default raised charcoal), OLED true black, Light, and Auto
(follows the OS and re-resolves live when it flips).
- Accent: an eight-hue wheel (cyan default) that drives the one data color
across charts, rails, focus rings, active states, and the ambient glow.
- Fine-tune sliders: a master Contrast that spreads page, ink, and borders
together, plus Border brightness and Ambient glow.
- Typography: swappable interface (Geist / IBM Plex Sans / Hanken Grotesk) and
data (Geist Mono / IBM Plex Mono / Fira Code) faces, and a text-size control
(continuous slider in Settings, S/M/L/XL presets in the popover, kept in
sync). The display serif stays locked as the signature face.
Surfaces, borders, and ink derive from per-theme lightness values so the live
knobs scale the whole UI through CSS, and the opaque directional borders read on
every panel including true black. A live preview reflects changes in real time.
Adds a documentation page under Features.
* fix(appearance): keep contrast-driven tokens in gamut and add picker keyboard nav
Clamp the contrast and knob driven surface, border, and ink lightness so the
full slider range stays a valid color. The page now always sits below the card
tone, so page/card separation no longer collapses at high contrast in Light; the
lit border edge never reaches white in Light at the low end of the knobs; and the
OLED and Dim extremes resolve to valid black/white instead of out-of-range values.
Add the radiogroup keyboard model (roving tabindex plus Arrow / Home / End) to the
accent and type pickers through a shared hook, matching the segmented control. One
item is tabbable and arrow keys move focus and selection together.
Lift <FleetActionCard> primitive from audit §20 / DESIGN §9.12. Migrate
the three v1 cards (stop-by-label, bulk-label-assign, prune-fleet-wide)
to consume it: cyan rail on every card, action class as a chip, blast
radius as a live readout in the toolbar, preview section replaces the
warning banner, footer carries reversibility plus freshness. Drop the
per-card tone rail, the icon prop, and cards/tone.ts.
Add /fleet/labels/match-preview and /fleet/prune/estimate for the live
blast readouts (chrome falls back to "preview unavailable" if either
404s). Add dryRun: true to the existing fleet-stop and fleet-prune
endpoints so the Dry run button rehearses the full code path (locks,
per-node fan-out, remote propagation) without firing the destructive
leaf call. Result flows into the existing ResultsList.
Extend <SheetSection> with optional meta. Add --action-transformative
semantic token.
- Remove redundant SENCHO prefix from kicker (already shown in card header)
- Animate the left accent bar with a slow back-and-forth shimmer sweep
- Replace static status dot with animated ping pulse
Apply design audit section 13's proposed chroma/lightness values to
frontend/src/index.css. Dark-theme accents now match the audit swatches
exactly: brand 0.78 0.11 195, success 0.78 0.16 155, warning 0.82 0.14
75, destructive 0.72 0.18 20. Surface hierarchy shifts to a warm pebble
cast (hue 80, chroma 0.003 to 0.01); card anchors to the audit's
0.14 0.005 80 surface swatch; popover bumps to 0.16 to preserve the 4
percent lightness gap against card. Light theme mirrors symmetrically.
NetworkTopologyView keeps the react-flow inline-style escape hatch but
hoists the brand literal into a BRAND_COLOR constant so MiniMap and
EDGE_COLORS share one source of truth.
Also normalize em-dash comment separators to colons across index.css
and drop a duplicated dark-background literal in favor of
var(--background).
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.
Extract the app header into a dedicated TopBar component and apply the
same translucent chrome language used by the sidebar: bg-sidebar +
backdrop-blur-md + inset top highlight. A new --chrome-top-highlight
token drives the highlight in both themes via the shadow-chrome-top
utility alias.
Restore the pulsing-dot unread indicator on the notification bell
(replaces the temporary count pill) and keep its aria-label dynamic so
screen readers still hear the unread count. Tighten nav a11y with
aria-current on the active item, aria-label on icon-only buttons, and a
proper <nav> landmark around the desktop Highlight group.
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.
- Repoint --chart-1..3 to brand/warning/destructive so cyan leads as the
primary data color, amber marks peaks, rose marks errors. --chart-4/5
fall back to neutral greyscale tones tuned per theme.
- Add --data-line-width (1.25px) and --data-peak tokens so sparklines
across dashboard, stack table, and readiness views share one source
of truth instead of re-deriving per component.
- Introduce five semantic label role aliases (env, role, owner, risk,
custom) mapped to existing hues. Raw ten-hue palette stays in place
for existing consumers; new labels should consume roles.
- Replace Georgia serif with Instrument Serif and add --font-display
alias in both :root and .dark; expose font-display via @theme inline
so Tailwind utility classes can use it. Load the family from Google
Fonts alongside Geist + Geist Mono.
* 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(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.
* feat: add RBAC viewer accounts, atomic deployments, and fleet-wide backups (Pro)
Introduces three Pro-tier features:
- RBAC: Multi-user system with admin/viewer roles, user management UI,
automatic migration from single-admin credentials, viewer restrictions
across the entire UI (read-only editor, hidden action buttons)
- Atomic Deployments: Pre-deploy file backup to .sencho-backup/, automatic
rollback on health probe failure, manual rollback button, health probes
added to stack updates, webhook-triggered deploys use atomic rollback
- Fleet-Wide Backups: Point-in-time snapshots of compose files across all
nodes (local + remote), stored centrally in SQLite, per-stack restore
with optional redeploy, graceful handling of offline nodes
* fix(settings): use correct ProGate prop name in UsersSection
* fix(settings): remove unused isPro prop from UsersSection
* fix(auth): fetch user info after login and setup so isAdmin is set correctly
* feat(pricing): revise pricing strategy and enforce variant-based seat limits
Raise Personal Pro from $49/yr to $69/yr with 3 viewer seats (up from 1).
Add $15/mo billing option for Team Pro. Mark lifetime pricing as a
90-day early-adopter offer. Store Lemon Squeezy variant_name on
activation/validation and enforce seat limits server-side per variant.
* feat(licensing): add Lemon Squeezy checkout, webhook, and billing portal integration
Server-side checkout URL generation (POST /api/checkout) with admin email
pre-fill and instance_id custom data. HMAC-SHA256 verified webhook endpoint
(POST /api/webhooks/lemonsqueezy) handling order, subscription, and payment
lifecycle events for automatic license activation. Customer billing portal
link stored from webhook events and exposed via GET /api/billing/portal.
In-app checkout buttons in Settings with manual license key fallback.
* fix(licensing): exempt Lemon Squeezy webhook from auth middleware
The catch-all auth middleware on /api/* was blocking the public webhook
endpoint. Added /webhooks/lemonsqueezy to the exemption list alongside
/auth/* and /webhooks/:id/trigger.
* feat(pricing): update pricing to final live rates
Personal Pro: $7.99/month, $69.99/year, $249 lifetime.
Team Pro: $49.99/month, $499.99/year, $1,499 lifetime.
Added personal_monthly checkout variant across backend, frontend, and website.
* refactor(licensing): remove server-side checkout/webhook for self-hosted model
Sencho is self-hosted — each user runs their own instance, so there is
no central server to receive webhooks or hold the store API key. Replaced
in-app checkout buttons with a "View Pricing" redirect to sencho.io and
kept manual license key activation as the primary flow.
- Delete LemonSqueezyService (checkout, webhook, HMAC verification)
- Remove POST /api/checkout, GET /api/billing/portal, POST /api/webhooks/lemonsqueezy
- Remove raw body parser and auth exemption for webhook route
- Remove all LEMONSQUEEZY_* env vars from .env.example
- Replace checkout buttons in SettingsModal with single "View Pricing" button
- Simplify LicenseContext checkout to open sencho.io pricing page
- Update licensing docs to reflect website-based purchase flow
* chore: normalize em-dashes to hyphens across codebase (linter)
* chore: remove accidentally tracked directories from index
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.