v5 drawers offered 1h/6h/12h history zoom; the v6 shared range list
started at 24 hours, so an operator investigating a spike from the last
hour got a day-wide chart. The backend chart range parser and the
HistoryTimeRange type already support the shorter windows; only the
shared HISTORY_CHART_RANGES list and the drawer labels were missing
them. StoragePoolDetail keeps its own narrower range list and is
unaffected. Flagged by the Machines page v5 parity audit; applies to
every drawer History tab.
The Machines page v5 parity audit (same lens as #1471 on the disks
table) found the rebuilt table dropped every threshold-aware signal v5
Hosts had, and the rebuild mislabeled one datum outright.
- Temperature cells render through the canonical TemperatureGauge
(muted / amber >=70 / red >=80) like the Proxmox nodes table; v6 had
been rendering plain neutral text, so a hot machine never stood out.
- CPU, memory, and disk bars receive per-resource alert thresholds via
hostOverrideIdCandidates + alertsActivation, mirroring the
Infrastructure host table; custom thresholds were ignored here.
- Network interface tooltips show cumulative RX/TX counters as bytes;
they were formatted as rates (a 16-day total read as 1.46 GB/s).
- Temperature tooltip sections cap at six rows with a visible +N more
marker instead of silently dropping readings (worst-first sort keeps
the alarming ones); the section title flattening skips empty values.
- The memory bar passes the agent-reported reclaimable cache through,
completing the used | cache | free split for machines.
f62f35e24 restored the v5 used | cache | free memory split for Proxmox
nodes and guests, but standalone host agents still reported a flat
used/free pair, so the Machines page memory bar could not show the
reclaimable segment. Flagged by the Machines page v5 parity audit.
- Host agent reports cacheBytes (gopsutil Available minus Free); the
ZFS ARC adjustment recomputes free so used + cache + free still
covers the total.
- ApplyHostReport maps the field into models.Memory.Cache and clamps
inconsistent or older-agent reports so used + cache never exceeds
total.
- AgentMemoryMeta carries cache onto unified resources so the frontend
agent payload exposes it.
- Mock generic hosts split a third of non-used pages as cache, and the
node-linked host conversion now holds the invariant instead of
stacking the node's cache on top of a recomputed free.
- Contracts: monitoring, unified-resources, and storage-recovery now
document the split (also covering the f62f35e24 node/guest surface,
which landed without contract deltas).
OpenCode syntax-highlights code in the transcript; Pulse rendered
fenced blocks as flat text. Assistant answers are full of shell
commands, configs, and query output, so add highlight.js with an
infra grammar set (bash, json, yaml, ini, sql, dockerfile, nginx):
- Lazy-loaded into its own vendor-highlight chunk; the entry chunk
stays clean and the chunk only downloads with the rest of the
preloaded app shell.
- Highlighting runs over the sanitized DOM after the turn settles, so
generated spans never pass through DOMPurify, streaming morphs are
never fought, and the class allowlist stays closed to the model. The
single carve-out is marked's fence-language hint, pattern-pinned to
language-x on <code> only.
- Token palette maps hljs classes to theme colors readable on the
dark prose-pre background both app themes share.
Per-block copy is intentionally not added: OpenCode has none, and the
answer-level copy already exists.
Assistant turns on slow routes run one to two-and-a-half minutes, and
operators tab away; OpenCode raises a desktop notification when a turn
completes, errors, asks a question, or waits on permission while the
terminal is blurred. Pulse now does the equivalent through the browser
Notification API behind an opt-in bell toggle in the drawer header:
enabling requests permission, a denial keeps the toggle off with
guidance, and the preference persists per browser.
Notifications fire only when the tab is hidden or unfocused, carry
fixed generic copy (OS notification centers persist text, so answers,
commands, and resource names never travel through them), coalesce per
kind, and click-to-focus. Dispatched once per stream event at the top
of processEvent, never inside store updaters.
v5 had 'Update All (N)' and per-host 'Check updates' one click away in
the Docker filter bar. The v6 rebuild moved those actions into the
generic resource drawer's docker-host section, but no surface in the
current IA mounts that drawer for a docker-host: the Docker page hosts
table uses DockerHostDrawer (counts only) and the Machines page does
not list container hosts. The actions were orphaned; the only way to
update containers was one row at a time.
Wire the existing useResourceDetailDrawerDockerActionsState hook into
the host drawer's Containers card: Check updates, Update all (N) with
confirm-click, queued-command status, and an updates-last-checked row.
Update all hides when the server disables Docker update actions and
disables while a host command is in flight or no updates are known.
The resource drawer gated its History tab on type === 'agent', hiding
per-container CPU/memory/disk/IO charts even though the metrics store
records them and the charts API maps app-container to the
dockerContainer/docker store keys end-to-end (metrics_targets.go hands
out the raw container ID precisely so those records resolve). v5
charted every container; v6 charted none.
- Gate the History tab on a resolvable metrics target instead of the
agent type. Resources without a target (images, volumes, networks)
keep no tab.
- Return current-value fallback metrics for any resource, not just
agents, so charts have a hint while the store fills.
- Add a Container section to the table-row summary: image, restart
count (red above the v5 crash-loop threshold), created-at, compose
project/service, and the full label set. All of it ships in the
payload; none of it rendered anywhere after the v5 drawer was
retired.
The v6 Docker page regressed against its v5 ancestor (same disease as
the physical disks table, #1471): container CPU/memory flowed all the
way into the unified payload but no column rendered them, so a container
pegged at 75% CPU looked identical to an idle one. Labels (compose
project/service, traefik rules) and the Swarm stack name shipped in the
payload but were unrendered and unsearchable.
- Add always-visible CPU and Memory metric-bar columns to the containers
table (ResponsiveMetricCell / StackedMemoryBar, same primitives as the
hosts table above it).
- Flag crash-loopers: restart counts above 5 render red, matching the
v5 threshold.
- Demote the Runtime column to mixed-runtime fleets only; it repeated
host-level data on every container row (the #1471 Source-badge
pattern) and the Host column sits right next to it.
- Add a Stack column to the Swarm services table, derived from the
serialized stack field's canonical com.docker.stack.namespace label.
- Make container/service labels and stack names searchable via the
Docker search haystack (v5 searched labels).
- Render volume Created as relative time with the absolute timestamp in
the tooltip instead of a raw ISO 8601 string.
The alert engine stamps metadata.resourceType on every alert, but the
websocket state path converts alerts.Alert to models.Alert, which had no
Metadata field, so every active alert reached the frontend stripped. The
history Type badge then fell back to unified-store lookups that miss
nodes (alert.resourceId is the platform-native node ID while unified
resources mint canonical ids, and alert.resourceName is the raw node
name while unified resources prefer the display name), rendering
Unknown. In mock mode the generated history rows had the same gap.
models.Alert gained the Metadata field in f62f35e24 (it rode along with
the memory-cache commit); this completes the transport:
- copy Metadata in activeAlertsSnapshot (websocket active alerts),
GetRecentlyResolved (resolved alerts to state), and the mock
UpdateAlertSnapshots conversion; sources are deep clones already
- deep-copy Metadata in models cloneAlert to keep the snapshot
clone contract honest
- stamp resourceType in the mock history generator using the real
engine vocabulary (node, vm, system-container)
- recognize system-container in the history Type badge map; that is
what the v6 engine stamps for LXC guests
Verified live in mock mode: history previously resolved 277 of 780
rows to Unknown (all node alerts); now 718/718 rows and 19/19 active
alerts carry resourceType and zero badges render Unknown.
The mock random-metrics updater recomputes Used/Free from the sampled
percentage but left Cache at its generation-time value, so a drifting
node could show used+cache > total and a 102% 'Shown in Proxmox' row.
Clamp the cache into the non-used pages in applyMemoryUsage, and teach
the memory-bar presentation to clamp defensively so a momentarily
inconsistent snapshot can never render segments past 100%.
v5 modeled memory as used | cache | free (Memory.Cache, 'reclaimable
buff/cache') and the memory bar's tooltip carried a 'Shown in Proxmox'
row explaining why Pulse's percentage reads lower than the Proxmox UI's
cache-inclusive number — a recurring support question. The v6 rebuild
deleted the field from the backend model, so the split and the
reconciliation vanished product-wide. Flagged by the Proxmox overview
parity audit.
Backend: re-add Memory.Cache; split it out via a shared
splitReclaimableMemory helper at the node resolver (node status reports
truly-free directly) and the VM builder (when guest meminfo reported
free pages); transport as proxmox.memoryCache on unified resources
alongside swap/balloon; mock mode populates plausible cache for nodes
and VMs.
Frontend: cache prop on StackedMemoryBar with the v5 muted-amber
segment between active and balloon, tooltip rows for reclaimable cache,
truly-free Free (balloon-capped), and the 'Shown in Proxmox'
reconciliation; guest and node memory adapters normalize free to
truly-free at the boundary; guest and node drawers grow a Reclaimable
cache row; the Proxmox nodes table passes cache and node swap through.
Three more findings from the v5-parity audit of the pools table, all
mirroring the disks-table fixes in 809e2c900:
- The Source column rendered an identical PVE badge on every row: the
surface's only live embedding (the Proxmox page) forces
source=proxmox-pve, so the column was pure noise. Drop it and surface
the record's platform as a Source row in the expansion's Configuration
card; the FilterBar source chip still covers unforced embeddings, and
persisted sort-by-source selections fold back to priority.
- PVE-configured PBS storage rows showed raw agent/storage resource ids
in the Host column because the backup-repository host-label chain
skipped the owning node and fell through to parentId. Name-bearing
candidates (parent name, PBS instance name, PVE node, storage nodes)
now come before raw ids.
- Rows tightened from 38px to the 32px platform table standard, and the
freed Source width redistributed across Storage/State/Type/Host/Usage.
Also drops the row model's never-rendered platformLabel/platformToneClass/
compactImpact/compactIssue/compactIssueSummary fields - the same
extracted-but-unrendered drift flagged in #1471.
Verified live in mock mode: headers Storage/State/Type/Host/Protection/
Usage/Growth, backup-vault hosts show pve1-3, expansion shows Source PVE,
rows measure 32px.
Below the 1440px wide breakpoint the workloads table layout-gated IP,
Node, Tags, OS, Net I/O, and Disk I/O with no recourse: the columns
menu still offered the toggles but flipping them changed nothing, and
v5's horizontal-scroll fallback was gone, so a 1280px laptop simply
could not see guest tags or I/O rates. Flagged by the Proxmox overview
parity audit; applies to every workloads surface.
- Track explicitly pinned columns per storage scope
(workloadsHiddenColumns[:scope]:forced); pinned columns render even
when the breakpoint gates them off and the table wrapper's existing
overflow-x-auto picks up the slack.
- Menu checkboxes now mirror what the table actually renders: a
layout-gated column reads as hidden, the first toggle pins it, the
second unpins it back to the layout default (without poisoning the
user-hidden set for wide viewports).
- Column reset clears pinned columns along with hidden ones.
The v5 clusters table read "2/3 ready" the moment a node went down;
the v6 rewrite flattened Nodes / Pods / Deployments to raw totals, so a
cluster with a NotReady node still showed a plain "3". Roll attention
counts (danger/warning indicator variants) into the per-cluster child
rollup and render healthy/total with the amber tone on shortfall.
Muted states (Unknown, deployments scaled to zero) deliberately do not
count as attention.
On a pure-Proxmox surface the polymorphic 'info' column always renders
the VMID; the generic 'Info' header was unified-model vocabulary leaking
into a platform page (v5 called it 'ID'). Use the existing
columnLabelOverrides hook. Flagged by the Proxmox overview parity audit.
Noise findings from the v5-parity audit:
- Eight tables carried copy-pasted scope helpers that preferred the
internal cluster id over the display name, so the pods table read
"k8s-production-1/services" while the deployments table said
"Production EU" for the same cluster. Replace every local helper
with shared kubernetesScopeLabel / kubernetesClusterLabel (name
first, id fallback) in the page model.
- The events Observed column rendered the raw ISO timestamp with
microseconds; humanize it like every other age on the page and keep
the raw timestamp on the hover title.
- Drop the Deployments Observed column: raw observedGeneration with no
spec generation beside it fails the what-does-the-user-do test, the
same shape as the Source badge removed from the disks table in
#1471. The value still ships in the resource payload.
v5's dashboard closed with a 'N running | N degraded | N stopped' strip
(Dashboard.tsx); the v6 rebuild kept computing those counts in
ProxmoxPageSummary — tests and all — but nothing ever rendered them,
the same flows-but-never-renders failure mode as #1471's wearout field.
Add a degradedGuestCount to the summary and close the overview with a
compact totals strip using the status-chip vocabulary (running /
attention / stopped). Flagged by the Proxmox overview parity audit.
v5 listed PBS instances in the dashboard nodes table with live CPU,
memory, and uptime; the v6 IA moved PBS to the Backups tab but its
server table carried only Status/Version/Datastore columns, leaving PBS
host health invisible on the entire Proxmox surface. The unified pbs
resource already ships the metrics, so add CPU, Memory (with byte
sublabel), and Uptime columns to the backup-servers table; they ride on
each of the server's datastore rows. Flagged by the Proxmox overview
parity audit.
The v5-parity audit found data reaching the browser and rendering
nowhere, same disease as the #1471 disks table:
- Per-container status (state, reason, readiness, restarts, image) was
only ever aggregated into x/y ready counts; v5 listed each container.
- Node OS image / kernel / architecture and the capacity vs allocatable
budget (cores, memory, pods) had zero consumers. For nodes without a
linked Pulse host agent the generic host section reads agent data
only, so the OS identity surfaced nowhere at all.
- The cluster API server endpoint was searchable but rendered nowhere;
pendingUninstall reached the browser and no surface used it.
Add a Kubernetes section to the resource detail drawer (pods: QoS +
per-container rows; nodes: OS identity + capacity/allocatable +
cordon state; clusters: API server / context / agent version / pending
uninstall), reusing the platform-neutral section shape introduced for
TrueNAS. Show pod capacity in the nodes table Capacity column and badge
pending-uninstall clusters in the clusters table.
The agent collects QoSClass and the legacy models carry it, but
resourceFromKubernetesPod dropped it, so no v6 surface could render
what v5's pod expansion showed. Additive optional field; proof added
to the kubernetes registry ingest test.
Found by the v5-parity audit of the Kubernetes page.
Every v5 NodeSummaryTable column header sorted; the v6 rebuild shipped
static headers. Restore click-to-sort on all columns except the Web
link: identity/text columns default ascending, metric columns default
descending, repeat clicks flip then clear back to the snapshot order,
and missing values (offline metrics, absent temperatures) stay last in
either direction. Flagged by the Proxmox overview parity audit.
The v6 nodes table dropped four glanceable signals its v5 ancestor
(NodeSummaryTable) carried: alerting rows lost their red/yellow accent
and background tint, pending apt updates lost the escalating badge in
the name cell, uptime was rounded to whole days, and offline nodes
rendered at full opacity. Flagged by the Proxmox overview parity audit.
- Wire getAlertStyles over the websocket alert store (alerts key on the
canonical unified resource id) and reuse the workload-row accent CSS
through a host-row variant.
- Pending-updates badge (orange at >=10, yellow below) beside the name.
- Full '26d 4h' uptime via the shared formatUptime; <1h warning kept.
- opacity-60 on offline rows, matching guest-row stopped dimming.
The v6 unified-resource node shim never read the PVE URLs the backend
ships (proxmox.guestUrl / proxmox.host): guestURL came from a
nonexistent customURL field and host carried the bare node name, so the
workloads group-header link resolved relative to the SPA
(/proxmox/pve1 in a new tab) and the nodes table had no link at all.
v5's dedicated link column (NodeSummaryTable) was silently dropped in
the rebuild. Flagged by the Proxmox overview parity audit.
- Map proxmox.guestUrl into guestURL with the proxmox.host API URL as
fallback in nodeFromResource; host stays a hostname label because the
node drawer renders it as one.
- Add getNodeExternalUrl: only URL-shaped values become link targets,
bare hostnames fall through to the canonical https://<node>:8006.
- Point NodeGroupHeader at the helper.
- Add a Web column (Machines-table pattern) to the Proxmox nodes table
at compact and wide layouts.
The v6 alerts history regressed against its v5 ancestor (same audit
pattern as the physical-disks table in 809e2c900, flagged from the
alerts-surface parity audit):
- The Type column silently swapped which-alert-fired (cpu, backup,
powered-off) for the unified resource kind (VM, LXC); the humanized
alert-type label was computed into HistoryItem.title and never
rendered. Render title in the Type column, colored by resource kind
with the kind as hover title - the exact v5 scheme.
- The #1218 stable id tiebreaker was dropped from both the overview
and history sorts, so alerts fired in the same polling cycle scramble
on re-render. Restore it in useAlertOverviewState and
applyAlertHistoryWindow, with regression tests.
- Duration, Status, and Node were hidden below lg with no recovery
path; v5 kept all columns reachable via horizontal scroll. Un-hide
them and give the table the v5 min-w-[800px] inside the shared
scroll wrapper.
- Drop the Source column: buildAlertHistoryItems only ever emits
source 'alert', so the badge was identical on every row (the 'ai'
Patrol merge never happens). Remove the dead presentation helper;
the source field stays on the model for the action-button guards.
- Treat LXC like CT/Container in the resource-kind badge palette
(live mock showed LXC rows falling to the gray bucket).
The canonical Kubernetes adapter reports every unhealthy-but-alive state
(cordoned node, not-ready container, pending pod, partially available
controller) as `warning`, but the platform page's filter triad only
accepted degraded/paused, so those rows matched none of the Healthy /
Degraded / Offline chips and were findable only under All. Flagged by
the v5-parity audit: v5's Unhealthy filter classified all of these.
- Accept `warning` into the page's degraded filter bucket.
- Declare `unschedulable` on ResourceKubernetesMeta (the adapter has
projected it all along) and map ready-but-cordoned nodes to an amber
Unschedulable indicator instead of green Ready. NotReady still wins.
- Render the mapped status label in the pods table status column. The
raw phase reads Running while a container is in CrashLoopBackOff;
the reason was previously visible only as the dot's hover title.
- Add `warning` to the frontend ResourceStatus union to match what the
backend emits.
OpenCode shows how full the model's context is next to the composer;
Pulse showed raw token counts with no sense of scale. The done event
now carries context_limit_tokens (providers.ContextWindowTokens for
the selected route) and the drawer's last-turn summary renders input
tokens as a percentage of the window: '8,500 tokens (6% of context)',
with the full breakdown in the hover detail. Without the limit the
summary renders exactly as before. Pinned in the plain-answer corpus
scenario.
Every Ceph-backed storage appeared twice on the pools table: once as the
PVE storage entry (online, mounted capacity) and once as the
cluster-internal pool row synthesized from Ceph telemetry (degraded, raw
pool accounting) - same name, conflicting state and usage, no visual
relationship. Found by the v5-parity audit; v5 rendered one row with a
cluster-health badge.
consolidateCephClusterPoolRecords now collapses the synthesized pool
rows (type ceph on the cluster pseudo-node) into the PVE rows that
mount them, matched by backing-pool detail or name. The pool row's
worse health is lifted onto the survivor (state cell, status detail,
issue summary) so a degraded cluster stays visible on the table, and
unmounted pool rows are kept so standalone-monitored clusters do not
lose their only capacity row. Raw per-pool accounting stays on the
Ceph tab's cluster drawer. Registry synthesis and pool alerting are
untouched.
Also pins the zfsSummary devices field in useStoragePoolDetailModel's
test, missed by b53e1a3e0.
While the model writes tool arguments the pending row showed only a
generic placeholder plus 'Receiving tool input.' for what can be tens
of seconds on slow routes. OpenCode streams the forming call into the
mutable tool part; Pulse now shows the live tail of the raw args in
the pending row whenever they are still unparsable, and drops it the
moment the input parses into a real summary or command preview. The
raw input was already carried by tool_progress events; this only
renders it.