The previous Storage card surfaced only the aggregated host.disk values,
which on multi-drive hosts (Unraid in particular) is just the rootfs and
misses the data drives entirely. Drawer drill-down should be more
informative than the row's hover tooltip, not less.
When the host's agent reports multiple disks, the Storage card now spans
two columns and lists each disk with mountpoint, percent, used/total, and
a threshold-coloured bar that matches the StackedDiskBar tooltip palette.
Single-disk hosts keep the existing aggregate Usage/Total/Free rows.
Replace inline `as { current?: number }` / `as { usage?: number }` casts on
the memory union with a `getNumericField` helper, and drop the stray
`agent()?.connectionState` lookup (that field lives on vSphere metadata,
not the agent telemetry record) so the Connection row reads the host
status directly.
Clicking a Docker host row in the platform-page hosts table now expands an
inline drawer with Overview (System / Runtime / Containers / Memory /
Storage / Telemetry, plus Swarm context when present) and History tabs.
The History tab reuses GuestDrawerHistory against the host's agent
telemetry, so utilisation, network I/O, disk I/O, and thermals share the
same backend and rendering primitives as the Proxmox NodeDrawer.
Drawer placement (inline TableRow with colspan) and keyboard activation
mirror ProxmoxNodesTable so users get the same drill-down affordance on
both platform pages.
Pre-push tsc rejected the discovery test fixtures because ResourceDiscovery
has more required fields than the minimum needed to exercise the new
identified-service helper. Widen the vi.fn return type so
mockResolvedValueOnce accepts partial-record fixtures, and cast the
fixtures through `unknown as ResourceDiscovery` (matching the existing
pattern used for the ports/facts subobjects in the same suites).
Discovery records carry rich identification (service name, category,
ports, cli access, paths) but until now they only rendered inside the
Discovery sub-tab. Operators opening a workload drawer to look at memory
or backups had no signal that Pulse already knows the LXC is "Homepage
Dashboard" or the Docker container is "Proxmox Backup Server" — they
had to switch tabs to see it.
Add an "Identified Service" card to GuestDrawerOverview that shows the
service name, category, port count, CLI access hint, and confidence
percent. The card mirrors the existing System/Guest Info/Backup card
shapes so it slots into the same flex grid.
useGuestDrawerState passively loads the discovery record via getDiscovery
(no scan triggered — manual scans, progress UI, and approval prompts
stay owned by DiscoveryTab and useDiscoveryTabState). A new presentation
helper, getDiscoveryIdentifiedSummary, packages the record into the
display shape and returns null when the record has no meaningful
identification — same gate as the Discovery tab's hasValidDiscovery, so
both surfaces render or hide together.
Contracts: amend frontend-primitives Completion Obligations to fence the
new identified-service reducer to discoveryPresentation.ts, and amend
performance-and-scalability Current State to require that out-of-tab
discovery surfacing stay passive (no scans). Verification via
discoveryPresentation.test.ts and GuestDrawer.test.tsx covers the
populated/empty/null branches and the drawer card render.
Workloads table row surfacing is the natural next step but blocked
behind the parallel agent's in-progress WorkloadPanel/WorkloadsSurface/
useWorkloadsState changes; revisit after those land.
Discovery wraps every probe in `docker exec <container> sh -c '...'`.
The agentexec command policy lists `^docker\s+exec\s` as RequireApproval
(a sound default for user-driven docker exec) and Discovery has no path
to mint or supply an ApprovalID. Result: every probe was rejected, the
scanner returned empty CommandOutputs, and the AI fell back to
"Unknown Infrastructure Resource" at confidence 0. The Discovery sub-tab
rendered empty after a "successful" run.
Add a Trusted bool to ExecuteCommandPayload on both the server-facing
agentexec type and the agent's wire struct. When set, the approval gate
is skipped on both ends and the server does not attempt to auto-mint an
approval grant (which would fail with "approval id is required").
PolicyBlock still applies; this is not a way to run arbitrary commands.
Only the discoveryCommandAdapter sets Trusted=true. The flag is never
populated from a deserialised HTTP body or any user-driven path. Patrol
fixes, Assistant remediation, and AI tool calls continue to flow through
the governed approval-record path with a real ApprovalID.
Contracts: amend agent-lifecycle Completion Obligations and Current
State to document the lone exception to the on-agent approval rail, and
amend ai-runtime to fence the Trusted flag to the discovery adapter
only.
Follow-up to the previous commit: resolveDiscoveryTargetForWorkload now
reads guest.containerId, so the Pick<WorkloadGuest, ...> input type must
include it or tsc rejects the property access.
For app-container workloads, resolveDiscoveryTargetForWorkload was
populating the discovery resourceId with guest.id — the frontend's
synthetic workload hash (e.g. "app-container-ed79032e5852577f"). The
backend wraps that as docker exec <id> sh -c '...', the Docker daemon
has no container by that name, so every scan came back with empty
stdout and the AI returned "Unknown Infrastructure Resource" at
confidence 0. To the user the Discovery sub-tab looked empty after a
"successful" run.
guest.containerId is already populated from resource.docker.containerId
for Docker-managed app-containers (useWorkloads.ts:450). Use it as the
discovery resourceId so the agent can actually docker exec into the
right container. VM and system-container branches were already correct
because they use vmid for pct exec / qm guest exec.
Fix the existing workloadTopology test that locked in the broken
behavior, and add containerId to the workloads.test.ts fixture that
exercises the same resolver branch.
The backend has had DiscoveryModel + GetDiscoveryModel() with the
shared-default fallback alongside ChatModel and PatrolModel, but the
settings UI only surfaced Assistant and Patrol overrides. Discovery
silently used whatever the shared default was set to, with no way to
dial it independently — yet Discovery is the surface where a cheaper
model (e.g. Haiku) is most often the right tradeoff.
Thread discoveryModel through the form state, load, save payload, and
failure analyzer, and render a third picker in the Model Overrides
section mirroring the Assistant/Patrol pattern. Generalize the section
header and shared-default copy so they no longer enumerate surfaces.
Extend the frontend-primitives contract to recognize Discovery alongside
Assistant and Patrol as a per-surface override, and lock the picker
presence into settingsArchitecture.test.ts.
Frontend canonicalizes Docker containers as app-container, but the
Discovery API only accepts docker. Without translation, every Discovery
trigger for a Docker container 400'd at type validation before any agent
dispatch — surfaced to users as "unsupported resource type 'app-container'".
Mirror the existing pod -> k8s translation in toDiscoveryAPIResourceType.
The agent transport layer already handles ResourceTypeDocker via "docker
exec" on the connected agent host; only the URL-path resource type needed
the alias.
The disk column was sized for the old "[bar] N% (used/total)" content
(~150-180px). With vertical micro-bars it only needs ~80-110px, so
the disk weight drops from 15 to 7 on desktop/compact (and the
proportional weights drop on mobile/tablet). Freed space redistributes
to Node/CPU/Memory/Cluster which all have more content to show.
Each vertical bar also gets a cursor-help cursor, a subtle ring on
hover, and a brightness/scale tick so users discover that bars are
individually inspectable — the native browser title tooltip on each
bar (disk name + percent + sizes) was already wired; this just makes
it visually discoverable.
Adds a 'vertical-bars' mode to StackedDiskBar and opts the hosts
table into it when a host has more than one disk. Each disk renders
as one 5px-wide vertical bar (fill height = utilization, color =
threshold), grouped and centred in the cell. All disks get identical
visual weight, no single disk is named or privileged, and 1-10 disks
all render the same way. Hover keeps the full per-disk breakdown.
Single-disk hosts still go through the original single-bar path.
The sub-row layout under each multi-disk host left the leading
columns (node/version/uptime/cpu/memory) blank on every disk row,
which fragmented the table visually and made the page feel empty
even when the disk content itself was readable. Going back to
single rows; the right fix needs more thought.
This reverts c0896fbc9 and 95758b6ef.
Previous version spanned from Disk all the way to Cluster, which gave
the per-disk bar far more room than it needs and visually fragmented
the table grid. Cap the colspan to Disk plus two right neighbours so
the sub-row sits in a sensible ~400px region, and fix the bar at w-20
instead of letting it stretch. Cts/Cluster columns now stay aligned
with the host row above.
The disk column was cramming N inline lanes into a ~197x16 cell, which
fell apart visually past three disks and forced label/percent collisions.
Hosts table is small (a handful of rows), so use the vertical space:
each multi-disk host gets one sub-row per disk beneath it, with the
sub-row colspan'd from the disk column through cluster so name, bar,
percent, and used/total all fit naturally. The host row's disk cell
collapses to a neutral "N disks" pill with a worst-threshold dot, so
nothing is hidden but no single disk is singled out by name.
The narrow-viewport card was rendering the row's status badge, problem
line, and Manage button as siblings inside a single flex-justify-between
row in the card footer. A long problem string ("Agent offline · 4h
ago", "Credentials invalid · 14 days") would squeeze the layout
unpredictably and push the Manage button into wrap territory.
Pull the problem line out of the flex row and onto its own line
beneath the status/Manage row, matching the wide-table treatment.
The SOURCE and ENDPOINT columns each carried a single static fact per
row (which collection method, which URL) that didn't help the user
scan for problems and that pushed the three columns the user actually
needed (System, Coverage, Status) into narrower lanes.
Fold source and endpoint into the System cell as a secondary line
beneath the name and subtitle: a small source badge inline with the
truncated endpoint URL. The cluster row, which has no endpoint, just
shows the source badge. Member rows follow the same pattern under
their indent.
New widths (System / Coverage / Status / Actions): 36 / 32 / 24 /
7rem at standard width, 34 / 34 / 22 / 7rem at xl. Section group
headers and lastError/empty-state rows colspan from 6/5 to 4/3.
Card layout (narrow viewport) is unchanged — it already stacks fields
vertically, not as table columns.
The agent-offline detail line was being produced by synthesizing a fake
FleetGovernanceSignal with key 'agent-attachment' and laundering it
through visibleFleetGovernanceSignals so the chip filter would pick it
up. With the chip stack gone and the row exposing a single
InfrastructureRowProblem, the synthesis is dead weight — it pretended
to be server-emitted governance state when it was really derived from
the agent's connection state.
- Replace agentAttachmentSignal with agentAttachmentProblem, returning
InfrastructureRowProblem directly. The row builder applies it ahead
of the fleet-highlights problem, so an offline agent always wins over
a downstream credential/config drift signal on the same row.
- Remove 'agent-attachment' from FleetGovernanceSignalKey now that no
signal carries it.
- Simplify the setup-summary Live/Needs attention counters to read off
the user-visible row state (statusLabel, problem) instead of
re-deriving via signal predicates. The previous path could
in principle disagree with what the row actually rendered.
- Drop rowFleetSignals, rowHasFleetTone, rowHasVisibleFleetTone — all
now unused.
Each platform overview page (Docker, Kubernetes, TrueNAS, vSphere)
composed its own <TableCardHeader title="Hosts" /> etc. independently,
so dropping the redundant title strips on Proxmox in the previous
commit didn't propagate. The architecturally correct fix is at the
primitive: the header strip only earns the chrome when it has real
content to show.
- TableCardHeader now renders null unless there's an `actions` slot or
an active `showClearAction`. Title-only is a silent no-op — every
platform-table caller stops showing a vestigial label automatically.
- `title` becomes optional on the props type so callers that intend to
use the strip purely for an action button (no caption) don't need to
pass a dummy.
- Title still renders when paired with actions, which keeps
StorageContentCard's "STORAGE / Storage|Physical Disks" header and
RecoveryProtectedInventorySection's "Protection coverage / Recovery
events" back-button strip working as before.
Net effect: Docker, Kubernetes, TrueNAS, vSphere overviews now match
Proxmox without touching any per-platform file, and future platform
tables can't accidentally re-introduce the strip by typing
title="Hosts".
TypeScript can't narrow FleetGovernanceSignal.tone through the find
predicates, so the helper now returns the literal tone for each branch
instead of forwarding the wider signal tone.
The Status column was rendering a stack of coloured chips drawn from
~ten independent fleet-governance enums, plus a synthetic "Fleet OK"
when none of them tripped. Source badge, status badge and chip array
could each be correct in isolation while telling competing stories on
the same row ("API + Agent" / "Active" / "Agent offline · 4h" all at
once). The canonical fix is to let the Status column tell one story.
- Add an `InfrastructureRowProblem` model field that names the single
most-important problem on a row in plain English, derived from the
prioritised fleet highlights via a new `primaryRowProblem` helper.
- Render that problem as a small italic detail line beneath the status
badge instead of as a parallel coloured chip. Critical problems read
rose, warnings amber; absence means nothing is wrong.
- Drop every `<For each={fleetHighlights}>` chip stack in the table —
four call sites across the row, member, and narrow-card layouts.
- Drop the "Install agent" nudge on non-PVE systems. PBS/PMG/TrueNAS/
vSphere are fully covered by their own API; the agent only adds host
telemetry, which is rarely the point of monitoring a backup server.
Scope the "Needs agent" counter the same way so the header summary
stops asking for an agent that isn't needed.
The fleet highlight signals stay on the row model for downstream
consumers (Manage drawer, audit views, tests) — only the parallel chip
render is gone.
- Move Bars/Sparklines toggle (and its sparkline-range sub-control) out
of the WorkloadsTable card header into the WorkloadsFilter's
viewOptionsTrailing, alongside Grouped/List, Charts, and Columns.
It's a view-mode control, not a per-row filter, and grouping it with
the other view options matches the FilterBar chip pattern.
- Drop the "Hosts" and "Workloads" title strips entirely from
ProxmoxNodesTable and WorkloadsTable. The column headers already
identify the tables (NODE/VERSION/UPTIME vs NAME/TYPE/INFO), the
FilterBar sits between them as a strong visual separator, and the
page header / route already names what the user is looking at.
- Relocate the pinned-summary "Clear" button (previously rendered as
an action in the now-removed WorkloadsTable header) into the
WorkloadsFilter viewOptionsTrailing, rendered conditionally via
pinnedSelectionActive() / onClearPinnedSelection.
- Tighten TABLE_CARD_HEADER_CLASS vertical padding (py-2 -> py-1) so
the remaining card-header strips (other platform tables, Recovery,
Storage) stop eating a wasted row of space.
Net effect on the Proxmox overview: Hosts and Workloads cards each
shed ~25-30px of header chrome. Same shared metricDisplayMode signal
drives both tables, same clearPinnedSummaryScope clears the same pin.
When a Pulse Agent attached to a Proxmox system goes silent, the row
kept advertising "API + Agent" while the chip below said "Agent offline".
Three signals on one row, two contradicting each other.
- Source badge now reflects what is currently contributing data, not what
has been configured. A connection only counts if its state is active
(or paused). When the agent goes silent, the badge collapses to "API"
and the existing "Agent offline · Xh ago" chip explains why.
- Cluster member rows always have an implicit API path via the cluster's
primary, so they now resolve to "API + Agent" when their paired agent
is contributing — previously they showed "Agent" only.
- Drop the duplicated standalone-agent row whose hostname matches a
cluster member elsewhere in the table. Same physical box, one row.
Read canonical Proxmox pveVersion metadata for host rows and share compact PVE version formatting across the Proxmox page and node headers. Add contract and proof coverage for the new Proxmox metadata fields.
The Connected systems table stacked four chips per row (Stale, Adapter
degraded, Remote control disabled, Fleet OK) drawn from independent fleet
enums, making it hard to see whether a system actually needs attention.
- Drop liveness chips from the highlight list; the row's status badge
already shows that state and was duplicating it.
- Drop the adapter chip when liveness is non-OK, since adapter health
degrades automatically once an agent stops heartbeating.
- Drop the info-tone "Remote control disabled/enabled" chip; default
policy state is not a posture warning and belongs in the Manage drawer.
- Stop synthesizing "Fleet OK" when nothing is wrong; an empty highlight
list paired with a green status badge says it more cleanly.
- Roll up the row state across attached components (not just cluster
members), so a stale paired agent surfaces on the parent row instead
of being hidden behind an Active PVE primary.