* fix: polish stack health table, security card heights, and scanner-gate copy
- Remove state dot column from Stack Health rows; state still signaled
via row tinting and sparkline color
- Reposition Update available badge adjacent to stack name and track
the theme accent (--brand) instead of static fuchsia
- Equalize Top exploit-risk findings and Severity x exploitability
card heights via h-full in a stretching grid
- Add resolution prop to CapabilityGate so scanner-gated tabs show
"Install a scanner from the Scanner setup tab." instead of the
misleading "Upgrade the node" message
* fix: scope Resources scroll area to table contents only
- Remove bg-background from the outer wrapper so search/filter bars
no longer sit on a dark strip
- Replace outer ScrollArea with a plain div so search and filter
controls stay fixed while only table rows scroll
- Add ScrollArea (max-h-[62vh]) inside each table card for Images,
Volumes, and Networks tabs
* fix: remove static bg-background from Security table scroll areas
Images and History tab tables used bg-background on their ScrollArea,
forcing a near-black fill that didn't adapt to the theme. Removing it
lets the parent card's bg-card show through, matching the Resources
table pattern that correctly respects DIM, OLED, and Light modes.
* fix: collapsible search icon and transparent Combobox across Resources and Security
- Replace always-visible search inputs on Resources tabs (Images,
Volumes, Networks) and Security Images (desktop + mobile) with
Fleet-style collapsible search: an icon button that expands to
a full input on click, collapses on blur when empty
- Add [&>button]:!bg-background to Security Images severity
Combobox, matching Fleet's transparent dropdown design
* fix: remove redundant orange badge from Unmanaged tab label
The count was shown twice: once as inline text in the tab and again
as an orange bg-warning pill badge. The inline text remains; the
duplicate badge is removed.
* fix: outline buttons invisible in Light mode
- Changed outline variant border from border-input to border so it
uses --card-border, which maintains a visible 0.035 lightness gap
from --background in Light mode (was 0.005, essentially invisible)
- Boosted Light-mode --button-inner-glow from a 4% white inset to
a 10% inset plus a subtle drop shadow for the physical-key feel
* fix: bump table cell horizontal padding from 8px to density-aware token
Changed TableHead and TableCell px-2 (8px) to px-[var(--density-row-x)]
which resolves to 20px comfortable / 16px compact. Matches the density
system used by StackHealthTable and gives every shadcn-based table
proper breathing room on both edges.
* fix: expand collapsible search before typing in ImagesTab test
The test was written for the old always-visible search input. Now that
the search bar collapses to an icon, click it first to expand before
interacting with the input.
* fix: reorder Settings Infrastructure to Nodes, Stacks, Fleet
Move the Stacks section directly below Nodes so the Infrastructure
sidebar reads Nodes, Stacks, Fleet. Pure array-element relocation in
the settings registry; the sidebar renders in array order.
* fix: simplify pilot enroll command and add Step 2 copy button
Step 2 of the pilot-agent enrollment modal now shows "docker compose up
-d" instead of the redundant "-f compose.yaml" form, since Step 1
already has the operator save the file as the default compose.yaml. Add
an inline copy button next to the command so it can be copied without
selecting the text by hand.
* fix: stack Fleet/Security tab heading actions on mobile
The shared FleetTabHeading kept its title and action buttons on one row
at every width, so on phones the Security Suppressions heading and its
Export VEX / Add suppression buttons were crammed together. Stack the
heading and actions vertically below the md breakpoint and restore the
original row layout at md and up, so desktop is unchanged.
* feat: add node selector to mobile Home masthead
The mobile Home page showed the active node as static text, so switching
nodes was only possible from the Stacks page. Render the same compact
NodeSwitcher in the Home masthead kicker slot, giving a second place to
view and switch the active node with the identical popover and Manage
nodes action.
CapabilityGate previously rendered the gated children behind a blur
filter and overlay pill when the active node lacked the required
capability. Combined with the lazy-loaded views from the recent
splitting work, this meant the chunk for FleetView, AuditLogView,
HostConsole, etc. fetched on click even when the user could not use
the feature, defeating the click-time IP protection the lazy split
was meant to deliver. CapabilityGate was the only always-leaking gate
in the app.
Replace the blurred-children render with a clean glass lock card that
explains the version mismatch ("Fleet Management is not available on
this node. <node> is running v0.42.0. Upgrade the node to use this
feature."). Children are no longer rendered, so the lazy children
never mount and no chunk fetch happens. All 13 consumers (5 full-page
views, 7 settings sections, 1 inline panel in ResourcesView) get the
new behavior automatically; no consumer-side changes required.
Extract a shared LockCard primitive used by both CapabilityGate and
the existing TierLockedCard inside settings/SectionGate. The two were
95% identical (same glass-card chrome, same icon framing, same text
hierarchy) and would have drifted as the design evolved. The shared
primitive accepts an icon, title, and body, with an optional className
for layout overrides; the inner geometry is fixed so every lock state
in the app shares the same visual rhythm.
PaidGate and AdmiralGate still fall through to "blurred preview with
small pill" after a user clicks Dismiss on their full-page upsell
(24h localStorage window). That post-dismissal click-time leak is
intentionally out of scope here; closing it would require either
removing the dismissal flow or replacing the blurred-children render
with a static placeholder, both UX decisions deserving their own PR.
* fix(fleet): resolve version detection pipeline for Docker builds
The Dockerfile backend-builder stage was missing a COPY of the root
package.json, causing generate-version.js to fall back to "0.0.0-dev"
at build time. At runtime, the filesystem walk also failed (root
package.json not in the final image), producing the string "unknown"
which the frontend rendered as "vunknown".
Changes:
- Dockerfile: copy root package.json into backend-builder stage
- CapabilityRegistry: return null (not "unknown") for unresolvable
versions; add isValidVersion() type guard; normalize remote meta
responses to strip "unknown"/"0.0.0-dev" sentinel values
- Fleet endpoints: hoist gateway version validation outside per-node
loops; treat unresolvable remote versions as "potentially outdated"
instead of silently marking them up to date
- FleetView: guard all version display points (card badge, update
button, gateway label, modal columns) via shared formatVersion()
- EditorLayout, CapabilityGate: use shared isValidVersion utility
- New frontend/src/lib/version.ts shared utility
- Docs: add troubleshooting section for version display edge cases
- Screenshots: updated Fleet Overview and Node Updates modal
* docs: update fleet node updates screenshot with live remote node
Treat 'unknown' and '0.0.0-dev' as absent versions in the node
switcher, fleet overview, and capability gate. Remote nodes running
older Docker images that return these fallback values now show a
clean label instead of "vunknown".
* feat(nodes): add capability-based node compatibility negotiation
Each Sencho instance now exposes /api/meta with its version and supported
capabilities. When the user switches nodes, the frontend fetches this
metadata and disables features the remote node doesn't support via a
CapabilityGate overlay. Version is shown in the node switcher dropdown
and connection test results.
- Backend: CapabilityRegistry with static capability list and fetchRemoteMeta helper
- Backend: /api/meta (public) and /api/nodes/:id/meta (auth) endpoints
- Frontend: NodeContext enhanced with per-node meta caching (5min TTL)
- Frontend: CapabilityGate component with typed Capability union
- Frontend: 13 features wrapped with capability gates
- Docs: node-compatibility.mdx + OpenAPI spec updates
* fix(nodes): revert to require() for package.json version reading
The static import fails in the Docker multi-stage build because the
root package.json is not copied into the backend-builder stage. The
require() call resolves at runtime when the file is available.