Commit Graph

14817 Commits

Author SHA1 Message Date
rcourtman e63da80b65 Colour Alert History frequency bars by severity
The frequency chart drew every period in one blue under a legend of
coloured severity dots that described nothing in the bars, left info
alerts out of that legend, and used a floored log scale on which a
25-alert burst looked barely three times one alert.

Each period now stacks critical, warning and info in the legend's own
colours, counted with the alerts contract's closed severity vocabulary,
and bars scale by the square root of their share of the busiest period so
a burst stands clear while quiet periods keep a readable mix. The legend
counts all three severities, and the tooltip and accessible name carry
each period's breakdown. Light-theme warnings use amber-700 because
yellow on the chart surface was under 2:1, and a selected period is
ringed instead of dimming the rest below 3:1.

The chart's two test files are registered as alerts frontend proofs.
2026-10-14 21:34:03 +01:00
rcourtman 477a81e603 Name Swarm rollouts truly and colour the cells behind each dot (#2952)
A Swarm service's dot read "Rollback paused" for Docker's
rollback_started and paused update states alike, contradicting the
Update column ("Rollback started"), and ignored rollback_paused. The
dot now says "Rolling back", "Update paused" or "Rollback paused", and
no replicas running outranks a rollout, since that service is down.

The cell behind each Swarm dot's verdict reads in its tone through the
shared dockerStateTextToneClass helper: a service's Running count (and
"/desired" on phones, which hide Desired) and its Update words when a
rollout needs a look; a task's Current state, now the dot's own word
with Docker's token and error on hover, and the error in the expansion
for a phone that cannot hover; a node's Reachability for an unreachable
manager and Availability for a drained or paused node. Desired reads
as words beside Current.

The task fixture's "running 2 minutes" state was never sent by the
agent (it reports task.Status.State); it now uses "running".

Reviewed by Sol from desktop and phone screenshots.
2026-10-14 21:15:05 +01:00
rcourtman f4aa5a7a3f Colour Kubernetes status words by their dot, from one platform helper (#2954)
A Pod's failure reason ("CrashLoopBackOff") sat in plain text beside a
red dot, cut to "CrashLoopBa…" at 1440px, and a phone, which hides the
Status column, showed only the dot. Deployments and controllers had the
same gap: a short Ready count and "1 not ready" read plain beside an
amber dot.

The tones the Docker tables use now live in one platform helper,
getPlatformStatusTextToneClass; dockerStateTextToneClass delegates to it
and PlatformIssueReason takes its colours from it. The Kubernetes tables
colour the cell behind each dot's verdict: a Pod's Status words, a
Deployment's Ready count while replicas are short, a controller's
Exceptions. The Pods Status column widens from 8% to 12% (from Node,
Restarts and Image) so CrashLoopBackOff reads whole, and a phone reads
the same words under the Pod name.

A new browser fixture, kubernetes-status-rows, covers the three tables.
Reviewed by Sol from desktop and phone screenshots.
2026-10-14 21:08:11 +01:00
rcourtman c91a7e4379 Let the truncation guardrail accept a toned block span
The platform summary-cell guardrail looked for the literal string
class="block truncate" in each table. The Swarm task's Current cell
now builds that class with its tone appended, which fails the literal
match while keeping exactly what the guardrail protects: a block span
that ellipsizes inside its own column. The check now accepts either the
plain or the template-literal form.
2026-10-14 20:53:17 +01:00
rcourtman f58f678398 Colour Kubernetes status words by their dot, from one platform helper
A Pod's failure reason ("CrashLoopBackOff") sat in plain text beside a
red dot, cut to "CrashLoopBa…" at 1440px, and a phone, which hides the
Status column, showed only the dot. Deployments and controllers had the
same gap: a short Ready count and "1 not ready" read plain beside an
amber dot.

The tones the Docker tables use now live in one platform helper,
getPlatformStatusTextToneClass; dockerStateTextToneClass delegates to it
and PlatformIssueReason takes its colours from it. The Kubernetes tables
colour the cell behind each dot's verdict: a Pod's Status words, a
Deployment's Ready count while replicas are short, a controller's
Exceptions. The Pods Status column widens from 8% to 12% (from Node,
Restarts and Image) so CrashLoopBackOff reads whole, and a phone reads
the same words under the Pod name.

A new browser fixture, kubernetes-status-rows, covers the three tables.
Reviewed by Sol from desktop and phone screenshots.
2026-10-14 20:46:35 +01:00
rcourtman 15c31e85ce Open a Proxmox storage alert's pool on the Storage page (#2953)
## What changes for users

On the Alerts overview, a Proxmox storage alert's name now opens that
pool on **Proxmox > Storage**, expanded with its capacity trend in view.
Before, it opened the top of the Proxmox overview and left the operator
to find the pool.

## How

- **Alert card**: Proxmox storage alerts link to
`/proxmox/storage?resource=<storage>`. TrueNAS and vSphere storage keep
their platform page, which does not read `?resource=`.
- **ZFS alerts**: pool-state, pool-error and device alerts use
`<storage>/zfs-pool:...` ids the registry cannot open, so
`checkZFSPoolHealth` stamps `storageResourceId`
(`TestZFSAlertsNameTheirStorage`). An alert raised before this change
opens Storage without naming a pool. A pool-error alert picks up the
stamp the next time its error count grows.
- **Storage deep links** (`useStorageResourceHighlight`):
- Matching order: record id, then metrics id, then a name only when it
is unique. Anything else resolves through `ResourceAPI.getResource`.
- Every matched pool now opens, not only Ceph, and the focus bridge
reveals it.
  - The hook acts only while Storage is the active Proxmox tab.
- A refused reference is retried on a later visit, not on every update.
- A pool the operator opens or closes while a lookup is running outranks
the link.

## Verification

- Hook and card tests: each new hook case fails against the previous
hook. The Storage, alerts, Proxmox and storageBackups suites pass, and
tsc and prettier are clean.
- `go test ./internal/alerts/`: the new test fails against the old
`storage.go`.
- Browser pass on a mock stack at 1280x800 and 375x812. The receipt is
included.
2026-10-14 20:37:33 +01:00
rcourtman eb42ada79a Open a Proxmox storage alert's pool on the Storage page
A full or degraded pool's alert linked to the top of the Proxmox overview,
so the operator had to find the pool themselves. The alert card now links
Proxmox storage alerts to /proxmox/storage?resource=, and Storage opens and
reveals that pool.

ZFS pool and device alerts are raised under composite ids the resource
registry cannot open, so checkZFSPoolHealth stamps the owning storage as
storageResourceId metadata and the card prefers it. An unstamped composite
id opens Storage without naming a pool.

The Storage deep-link hook matched only Ceph pools by id or any name, so
alert source ids and shared names (every node has a local-zfs) missed or
opened the wrong pool. It now matches id, then metrics id, then a unique
name, resolves anything else through the registry, and opens every matched
pool. It acts only while Storage is the active Proxmox tab, because the
mounted-but-hidden Storage tab otherwise read guest links, and a pool the
operator opens or closes during a lookup outranks the link.
2026-10-14 20:05:26 +01:00
rcourtman 3a7304d626 Name Swarm rollouts truly and colour the cells behind each dot
A Swarm service's dot read "Rollback paused" for Docker's
rollback_started and paused update states alike, contradicting the
Update column ("Rollback started"), and ignored rollback_paused. The
dot now says "Rolling back", "Update paused" or "Rollback paused", and
no replicas running outranks a rollout, since that service is down.

The cell behind each Swarm dot's verdict reads in its tone through the
shared dockerStateTextToneClass helper: a service's Running count (and
"/desired" on phones, which hide Desired) and its Update words when a
rollout needs a look; a task's Current state, now the dot's own word
with Docker's token and error on hover, and the error in the expansion
for a phone that cannot hover; a node's Reachability for an unreachable
manager and Availability for a drained or paused node. Desired reads
as words beside Current.

The task fixture's "running 2 minutes" state was never sent by the
agent (it reports task.Status.State); it now uses "running".

Reviewed by Sol from desktop and phone screenshots.
2026-10-14 19:56:40 +01:00
rcourtman fcc3e695f3 Link connected systems to the page that shows their data (#2950)
Settings > Infrastructure lists each connection with its health and a
Manage button, but nothing pointed to where its data appears. In #2905 a
user saw their PBS as Active there and could not find it in monitoring,
because PBS servers live under Proxmox > Backups.

A connected API system's name now links to the page that shows its data:

| Connection | Opens |
| --- | --- |
| Proxmox VE (incl. clusters) | `/proxmox/overview` |
| PBS | `/proxmox/backups` |
| Mail Gateway | `/proxmox/mail` |
| vCenter | `/vmware/overview` |
| TrueNAS | `/truenas/overview` |
| Docker / Kubernetes connections | `/docker/overview`,
`/kubernetes/overview` |

Agent hosts and availability checks get no link: an agent host appears
on Proxmox, Machines or Kubernetes depending on what it runs, so a fixed
destination would often be wrong. The name is underlined at rest (touch
users have no hover hint), keeps its existing tooltip, links in
read-only mode too, and the width-budgeted Actions column is unchanged.
It is a plain anchor that the router handles in-app.

Verification: browser receipt
`frontend-modern/browser-verification/494c7754ed93aba7.json` (1440
dark/light and 375 light; real click navigates in-app with no reload,
Back returns). Settings suites pass (2441 tests); the new component test
fails on the previous code.
2026-10-14 19:49:52 +01:00
rcourtman 02d21333c3 Merge remote-tracking branch 'origin/main' into fix/infrastructure-system-links 2026-10-14 19:30:26 +01:00
rcourtman 3bf85f166a Move the connected-systems note clear of main's runtime note
Both branches appended a dated section at the end of agent-lifecycle.md, so
merging main conflicted. Taking main's file with this branch's note one
section earlier keeps both notes and lets main merge cleanly.
2026-10-14 19:25:05 +01:00
rcourtman aa9548cf78 Let a paid community install switch to the Pro runtime (#2949)
A paid key on the public community build unlocks only the licence-gated
features compiled into it. Patrol modes, container update risk, Audit,
RBAC and Reporting need the private Pulse Pro runtime, and until now the
only route there was the download portal. Licence data from 14 October
shows 6 of 36 v6 buyers and 39 of 109 migrated v5 licences still running
only the community build.

This adds the backend for an in-app switch:

- `GET /api/updates/pro-runtime` (admin, `settings:read`) reports
whether this install can switch, and how. The license server download
broker decides eligibility from the activation's licence. It is the same
broker the Pro binary already updates from. Without an activation the
check answers `not_activated` with no network call.
- `POST /api/updates/pro-runtime/apply` (admin, `settings:write`)
installs the Pro build in place on systemd and Proxmox LXC installs. It
goes through `ApplyUpdate` with an explicit `SwitchToProRuntime` flag,
so ordinary community updates never take the broker path. It keeps the
signed archive, manifest hash, pre-install self-test, backup, readiness
gate and history rollback of a normal update. It accepts the Pro build
of the running version or newer and refuses an older pin.
- Docker installs get the broker's digest-pinned image commands in
`docker` instead.

The Settings → Updates card that uses these routes follows in a separate
pull request.

Tests: `internal/updates` (switch check and apply against a fake
broker), handler, scope, inventory and payload snapshot tests in
`internal/api`, the mutation registry audit, and the API reference
check. The full `internal/api` and `pkg` suites ran on pulse-dev. The
only failures were `TestServerInfoEndpointReportsDevelopment`, which
fails in any tree without `.git`, and a load-timing configapi test that
passed three times when run alone.
2026-10-14 18:52:39 +01:00
rcourtman 474efcee86 Link connected systems to the page that shows their data
Settings > Infrastructure lists each connection with a health state and a
Manage button, but nothing pointed to where its data appears. A user saw
their PBS as Active there and could not find it anywhere in monitoring,
because PBS servers live under Proxmox > Backups (#2905).

A connected API system's name now links to its page: Proxmox VE to the
Proxmox overview, PBS to Proxmox > Backups, Mail Gateway to Proxmox >
Mail Gateway, vCenter, TrueNAS, Docker and Kubernetes to theirs. Agent
hosts and availability checks get no link, because an agent appears on
different pages depending on what it runs. The link is underlined at
rest so touch users can see it, works in read-only mode, and leaves the
width-budgeted Actions column unchanged.
2026-10-14 18:23:51 +01:00
rcourtman 72bfeee246 Let a paid community install switch to the Pro runtime
A paid key activated on the public community build unlocks only the
licence-gated features compiled into that build. Patrol modes, container
update risk, Audit, RBAC and Reporting need the private Pulse Pro
runtime, and the only way to get it was the download portal. Licence
data from 14 October shows 6 of 36 v6 buyers and 39 of 109 migrated v5
licences still running only the community build.

GET /api/updates/pro-runtime reports whether this install can switch,
and POST /api/updates/pro-runtime/apply performs it. The license server
download broker decides eligibility from the activation's licence, the
same broker the Pro binary already updates from, so a refusal there
reads as not_eligible and any other failure as unavailable. Without an
activation the check answers not_activated without a network call.

The in-place switch is limited to systemd and Proxmox LXC installs. It
runs through ApplyUpdate with an explicit SwitchToProRuntime flag, so
ordinary community updates never take the broker path. It accepts the
Pro build of the running version or newer, refuses an older pin, and
keeps the signed-archive, hash, self-test, backup and history-rollback
steps of a normal update. Docker installs get the broker's
digest-pinned image commands instead. The Settings card that uses these
routes follows separately.
2026-10-14 18:20:02 +01:00
rcourtman 6f7926fddf Colour a network's attached-container state words by their dot (#2948)
Inside a Docker network's expansion, each attached-container card showed
its state words ("Unhealthy", "12 restarts") in plain text beside a red
or amber dot. They now use the shared dockerStateTextToneClass helper,
the same tones as the containers table's State words: red for a
problem, amber for a warning, muted for stopped, plain for running.

Reviewed by Sol from desktop and phone screenshots.
2026-10-14 18:15:28 +01:00
rcourtman 90ec416d2f Colour a network's attached-container state words by their dot
Inside a Docker network's expansion, each attached-container card showed
its state words ("Unhealthy", "12 restarts") in plain text beside a red
or amber dot. They now use the shared dockerStateTextToneClass helper,
the same tones as the containers table's State words: red for a
problem, amber for a warning, muted for stopped, plain for running.

Reviewed by Sol from desktop and phone screenshots.
2026-10-14 17:54:00 +01:00
rcourtman 03de664ec7 Give a Docker network row one dot for what is on it (#2947)
A network row showed two dots: a green one on its name, from the network
resource's own status (always online unless its host is lost), and a red
one beside "1 needs attention". The health chips filtered on the green
one, so that network counted as Healthy.

The row now has one dot, in the name cell: the network's own status when
that is worse, otherwise amber while anything attached needs attention,
otherwise green. Amber because the network itself is up; each attached
container keeps its own red or amber dot in the expansion. The State
words take that tone without a second dot, and the Healthy / Degraded /
Offline chips filter by the same verdict.

Docker State words on both tables now come from one tone helper,
dockerStateTextToneClass, so the containers table looks the same.

Reviewed by Sol from desktop screenshots.

Stacked on #2946, whose commit it carries until that merges.
2026-10-14 17:41:26 +01:00
rcourtman 17b8dd33a9 Filter a network's running containers by Docker's run state (#2946)
A Docker network's attached-containers Running filter matched only
rows with a green dot, so a container that was running but unhealthy
or restarting too often dropped out of "Running" entirely.

Attention stays the rows' verdict; Running and Other now follow
Docker's run state, so such a container matches Running and Attention
both. The sections stay one per row, Attention first, so the Running
view lists it under its Attention heading. Other reads "not known to
be running", which covers a container with no reported state.

The network's State counted only red rows ("1 needs attention" beside
an Attention chip of 3); it now counts every Attention row, the dot
taking the worst. On phones the Subnets cell and the Driver header were
hidden, so "bridge" sat under SUBNETS; the compact layout now drops
both columns together, leaving them to Network details and giving
State its room. The attached-containers header shows the total, and
the filtered count reads once, in the toolbar.

The browser fixture gains a Networks view over the existing containers.
Reviewed by Sol from desktop and phone screenshots.

Stacked on #2945 (and #2944), whose commits it carries until they merge.
2026-10-14 17:27:47 +01:00
rcourtman bb627673c1 Give a Docker network row one dot for what is on it
A network row showed two dots: a green one on its name, from the network
resource's own status (always online unless its host is lost), and a red
one beside "1 needs attention". The health chips filtered on the green
one, so that network counted as Healthy.

The row now has one dot, in the name cell: the network's own status when
that is worse, otherwise amber while anything attached needs attention,
otherwise green. Amber because the network itself is up; each attached
container keeps its own red or amber dot in the expansion. The State
words take that tone without a second dot, and the Healthy / Degraded /
Offline chips filter by the same verdict.

Docker State words on both tables now come from one tone helper,
dockerStateTextToneClass, so the containers table looks the same.

Reviewed by Sol from desktop screenshots.
2026-10-14 17:20:17 +01:00
rcourtman 394209d505 Colour a crash-looping container's dot by its restart verdict (#2945)
A running container Docker had restarted more than five times read four
ways: a green row and drawer dot, amber "N restarts" words on phones, a
red count in the wide Restarts column and a red lifted drawer row. The
attention-first sort ranked it with healthy containers.

mapDockerContainerStatus now owns one restart verdict: running past five
restarts is warning, "N restarts". The row dot, its title, the drawer
header dot and the sort read it in every layout. Where a row has a
Restarts column the count reads amber there and State keeps Docker's own
word; on phones State says "N restarts". The drawer's Restarts row is
amber, and its observed state stays Docker's word so the restart is
lifted once.

Amber rather than red because the container is running. Docker's count
is a total rather than a rate, which the hovers now say; the alert
engine still owns the windowed restart-loop alert.

Reviewed by Sol from desktop and phone screenshots.

Stacked on #2944, whose commit it carries until that merges.
2026-10-14 17:08:52 +01:00
rcourtman ac18c1d958 Filter a network's running containers by Docker's run state
A Docker network's attached-containers Running filter matched only
rows with a green dot, so a container that was running but unhealthy
or restarting too often dropped out of "Running" entirely.

Attention stays the rows' verdict; Running and Other now follow
Docker's run state, so such a container matches Running and Attention
both. The sections stay one per row, Attention first, so the Running
view lists it under its Attention heading. Other reads "not known to
be running", which covers a container with no reported state.

The network's State counted only red rows ("1 needs attention" beside
an Attention chip of 3); it now counts every Attention row, the dot
taking the worst. On phones the Subnets cell and the Driver header were
hidden, so "bridge" sat under SUBNETS; the compact layout now drops
both columns together, leaving them to Network details and giving
State its room. The attached-containers header shows the total, and
the filtered count reads once, in the toolbar.

The browser fixture gains a Networks view over the existing containers.
Reviewed by Sol from desktop and phone screenshots.
2026-10-14 17:04:07 +01:00
rcourtman 1f084498d7 Read a container's state under its name on narrow phones (#2944)
Below a 360px table (most phones at 360-393px) the shared shell gives
the name column a 40% floor, and the State column beside it, both
metric pills and the Update control cut "Unhealthy" to "Unhe…" and
"Exited (139)" to "Exite…". Narrowing the name from the colgroup does
not help: the browser takes the larger of the header and column widths
and rescales everything else.

The narrow layout drops the State column and shows the same words, tone
and title on a second line under the container name, on every row so
rows keep one height. CPU, memory and Update share the rest, which
keeps a 100% pill and the Update label whole at 320px too. The phone
layout and up keep their State column.

A saved sort now applies only while its column's header shows, so a
State sort stops reordering phone rows with no visible indicator; this
replaces the grouped view's Host/Engine special case. The name cell
only adds its column wrapper when a table passes a second line.

Reviewed by Sol from before/after screenshots at 320, 390 and 430px.
2026-10-14 16:59:13 +01:00
rcourtman 4c82d303cd Colour a crash-looping container's dot by its restart verdict
A running container Docker had restarted more than five times read four
ways: a green row and drawer dot, amber "N restarts" words on phones, a
red count in the wide Restarts column and a red lifted drawer row. The
attention-first sort ranked it with healthy containers.

mapDockerContainerStatus now owns one restart verdict: running past five
restarts is warning, "N restarts". The row dot, its title, the drawer
header dot and the sort read it in every layout. Where a row has a
Restarts column the count reads amber there and State keeps Docker's own
word; on phones State says "N restarts". The drawer's Restarts row is
amber, and its observed state stays Docker's word so the restart is
lifted once.

Amber rather than red because the container is running. Docker's count
is a total rather than a rate, which the hovers now say; the alert
engine still owns the windowed restart-loop alert.

Reviewed by Sol from desktop and phone screenshots.
2026-10-14 16:30:07 +01:00
rcourtman 3c9d36172a Read a container's state under its name on narrow phones
Below a 360px table (most phones at 360-393px) the shared shell gives
the name column a 40% floor, and the State column beside it, both
metric pills and the Update control cut "Unhealthy" to "Unhe…" and
"Exited (139)" to "Exite…". Narrowing the name from the colgroup does
not help: the browser takes the larger of the header and column widths
and rescales everything else.

The narrow layout drops the State column and shows the same words, tone
and title on a second line under the container name, on every row so
rows keep one height. CPU, memory and Update share the rest, which
keeps a 100% pill and the Update label whole at 320px too. The phone
layout and up keep their State column.

A saved sort now applies only while its column's header shows, so a
State sort stops reordering phone rows with no visible indicator; this
replaces the grouped view's Host/Engine special case. The name cell
only adds its column wrapper when a table passes a second line.

Reviewed by Sol from before/after screenshots at 320, 390 and 430px.
2026-10-14 16:03:01 +01:00
rcourtman db80bddd77 Colour a container with no reported state by its unified status (#2938)
The container status mapper reads Docker's own container state. When a
container reported none, the unified status stood in, but only "online"
and the stopped states were understood, so a degraded or offline
container fell through to a grey "Unknown"-style dot in its row, its
State words and its drawer header, while every other resource reads
offline as red and degraded as amber.

With no reported state (a blank one counts as none), lost-contact
statuses now read danger and degraded, warning, partial and recovering
read warning; unknown stays muted, and a reported state such as exited
still wins. Row dot, State words and drawer header share the mapper, so
all three move together.

Reviewed by Sol from desktop and phone screenshots; its blank-state
normalisation point is fixed here.


Stacked on #2937 (same Docker proof fixture); it carries that commit
until #2937 lands.
2026-10-14 15:47:18 +01:00
rcourtman 5dcd16bf6f Offer the update itself beside an available update in Worth checking (#2937)
A container drawer lifted "Image update: Available" into Worth checking
but offered no next step; the update control lived only in the table's
Updates column, which a phone row hides.

The lifted row now carries the same shared Update button as the table,
both built from one helper (getDockerContainerUpdateAction), so
readiness and server refusal read the same in both places, and a
"View image tags" link to see the new image. In the drawer the button
is 44px tall below sm (a new touchTarget option); tables keep the
compact pill.

The drawer rebuilds its inventory rows on every refresh and clock tick,
which would recreate the button and close an open review dialog. The
controls are now one element created once under the drawer's owner and
reused by every rebuild; tests hold a review dialog open across a
refresh and a minute of clock time.

Reviewed by Sol from desktop and phone screenshots; its points (tap
size, stability, a fixture showing real rather than loading states,
wrapping inside the card) are all addressed.
2026-10-14 15:35:14 +01:00
rcourtman 861d0a94b3 Colour a container with no reported state by its unified status
The container status mapper reads Docker's own container state. When a
container reported none, the unified status stood in, but only "online"
and the stopped states were understood, so a degraded or offline
container fell through to a grey "Unknown"-style dot in its row, its
State words and its drawer header, while every other resource reads
offline as red and degraded as amber.

With no reported state (a blank one counts as none), lost-contact
statuses now read danger and degraded, warning, partial and recovering
read warning; unknown stays muted, and a reported state such as exited
still wins. Row dot, State words and drawer header share the mapper, so
all three move together.

Reviewed by Sol from desktop and phone screenshots; its blank-state
normalisation point is fixed here.
2026-10-14 15:29:40 +01:00
rcourtman e787ca857d Offer the update itself beside an available update in Worth checking
A container drawer lifted "Image update: Available" into Worth checking
but offered no next step; the update control lived only in the table's
Updates column, which a phone row hides.

The lifted row now carries the same shared Update button as the table,
both built from one helper (getDockerContainerUpdateAction), so
readiness and server refusal read the same in both places, and a
"View image tags" link to see the new image. In the drawer the button
is 44px tall below sm (a new touchTarget option); tables keep the
compact pill.

The drawer rebuilds its inventory rows on every refresh and clock tick,
which would recreate the button and close an open review dialog. The
controls are now one element created once under the drawer's owner and
reused by every rebuild; tests hold a review dialog open across a
refresh and a minute of clock time.

Reviewed by Sol from desktop and phone screenshots; its points (tap
size, stability, a fixture showing real rather than loading states,
wrapping inside the card) are all addressed.
2026-10-14 15:16:45 +01:00
rcourtman 4bb2ce4e70 Colour a container drawer's status dot by its health, as the row does (#2935)
A running container failing its health check showed a red dot and
"Unhealthy" in its table row but a green dot in the expanded drawer
header, which used the generic run-status mapper for every resource.

The drawer header now uses the container row's mapper for app
containers, so run state and health check decide the dot and its label
in both places. Other resources keep the shared run-status dot. Sol
reviewed the desktop and phone screenshots and found the dot plus the
lifted "Health check: Failing" row enough, without another label.


Stacked on #2934 (same drawer); it carries that branch's commits until
#2934 lands.
2026-10-14 14:53:22 +01:00
rcourtman 40af02f9f9 Give agent, container and Docker host drawers the Proxmox Overview (#2934)
The Proxmox guest and node drawers lead with alerts, lift anything wrong
into Worth checking, show the charts, one facts line and a closed
Details
section. The other object drawers still opened large always-visible
cards: an agent's Operator Context card holding only an IP, a
container's digests and label inventory, five Docker host cards that
mostly restated the row.

The shared resource drawer and the Docker host drawer now follow the
same Overview. A "No active alerts" line, rows lifted in front, the
charts, a facts line that adds what the row lacks (container image and
Compose service; host CPUs, RAM and disk used of total, address), and
every inventory section, provider Platform details included, under one
closed Details.

Lifting follows evidence, not only colour: an available image update
carries an explicit lift flag; an offline resource's state is
danger-toned with when it was last seen, so it is never hidden behind
Details; a row the resource's own alert already states stays in Details
instead of repeating it. Facts state used amounts only for an online,
current reading. An availability endpoint keeps its check cards in
front, since they are its whole reading.

Reviewed twice by Sol from before/after screenshots at desktop and
phone width; the second pass confirmed the first round's findings
resolved and led to the alert de-duplication.
2026-10-14 14:44:11 +01:00
rcourtman 48f0dc9e82 Colour a container drawer's status dot by its health, as the row does
A running container failing its health check showed a red dot and
"Unhealthy" in its table row but a green dot in the expanded drawer
header, which used the generic run-status mapper for every resource.

The drawer header now uses the container row's mapper for app
containers, so run state and health check decide the dot and its label
in both places. Other resources keep the shared run-status dot. Sol
reviewed the desktop and phone screenshots and found the dot plus the
lifted "Health check: Failing" row enough, without another label.
2026-10-14 14:38:24 +01:00
rcourtman e72df20395 Expect a Job's failed count in Worth checking, not its section
The drawer now lifts warning-toned rows in front of its charts, so a Job's Failed count moves out of the controller section into Worth checking.
2026-10-14 14:26:31 +01:00
rcourtman b6e1fd18bc Give agent, container and Docker host drawers the Proxmox Overview
The Proxmox guest and node drawers lead with alerts, lift anything wrong
into Worth checking, show the charts, one facts line and a closed Details
section. The other object drawers still opened large always-visible
cards: an agent's Operator Context card holding only an IP, a
container's digests and label inventory, five Docker host cards that
mostly restated the row.

The shared resource drawer and the Docker host drawer now follow the
same Overview. A "No active alerts" line, rows lifted in front, the
charts, a facts line that adds what the row lacks (container image and
Compose service; host CPUs, RAM and disk used of total, address), and
every inventory section, provider Platform details included, under one
closed Details.

Lifting follows evidence, not only colour: an available image update
carries an explicit lift flag; an offline resource's state is
danger-toned with when it was last seen, so it is never hidden behind
Details; a row the resource's own alert already states stays in Details
instead of repeating it. Facts state used amounts only for an online,
current reading. An availability endpoint keeps its check cards in
front, since they are its whole reading.

Reviewed twice by Sol from before/after screenshots at desktop and
phone width; the second pass confirmed the first round's findings
resolved and led to the alert de-duplication.
2026-10-14 14:00:23 +01:00
rcourtman 86a244bc04 Name where History readings came from instead of always "stored" (#2933)
A History reply carries its source: the history store, readings Pulse
holds in memory because the store has none for the range, or a lone
current reading when no history exists yet. The drawer readout ignored
it and always said "Latest stored", so memory and live readings were
presented as stored history. The canvas charts did badge the source, but
as "Buffer" and "Live", words a reader has no reason to know.

One shared vocabulary now covers both. Stored history (and demo data)
keeps "Latest stored" / "Stored". Memory readings read "Latest recorded"
/ "Recorded", with one line above the drawer charts saying the store has
none for this range yet, and a "Recent only" badge on canvas charts. A
lone current reading reads "Current", badged "Current only".


Stacked on #2932 (same chart files); it carries that commit until #2932
lands.
2026-10-14 13:20:38 +01:00
rcourtman 59ed65739c Band and fold the storage History charts like the drawer charts (#2932)
The drawer charts gained a min-max band (#2917) and a phone fold
(#2923); the storage disk and pool charts, drawn by the canvas
HistoryChart, had neither.

Band: each stored point is a bucket average with its lowest and highest
reading. When any bucket spreads, the chart now draws a 20% band between
them behind the line, so a short spike survives downsampling. The area
fill under the line is gone, so storage charts read like the drawer
charts; a flat series is a line alone.

Phone fold: a disk detail stacked five charts on a phone. It now shows
the first History chart plus any chart whose SMART card is out of
bounds, and folds the rest, Live I/O included, behind one button. Folded
charts read nothing until opened. The button is a shared
PhoneChartsToggle that the drawer History now uses too.

The visual pass also caught short windows repeating a time label
("Oct 2" four times); the time axis now states each label once.
2026-10-14 13:06:16 +01:00
rcourtman f872ec9553 Name where History readings came from instead of always "stored"
A History reply carries its source: the history store, readings Pulse
holds in memory because the store has none for the range, or a lone
current reading when no history exists yet. The drawer readout ignored
it and always said "Latest stored", so memory and live readings were
presented as stored history. The canvas charts did badge the source, but
as "Buffer" and "Live", words a reader has no reason to know.

One shared vocabulary now covers both. Stored history (and demo data)
keeps "Latest stored" / "Stored". Memory readings read "Latest recorded"
/ "Recorded", with one line above the drawer charts saying the store has
none for this range yet, and a "Recent only" badge on canvas charts. A
lone current reading reads "Current", badged "Current only".
2026-10-14 13:04:24 +01:00
rcourtman 8ae88cb6d1 Band and fold the storage History charts like the drawer charts
The drawer charts gained a min-max band (#2917) and a phone fold
(#2923); the storage disk and pool charts, drawn by the canvas
HistoryChart, had neither.

Band: each stored point is a bucket average with its lowest and highest
reading. When any bucket spreads, the chart now draws a 20% band between
them behind the line, so a short spike survives downsampling. The area
fill under the line is gone, so storage charts read like the drawer
charts; a flat series is a line alone.

Phone fold: a disk detail stacked five charts on a phone. It now shows
the first History chart plus any chart whose SMART card is out of
bounds, and folds the rest, Live I/O included, behind one button. Folded
charts read nothing until opened. The button is a shared
PhoneChartsToggle that the drawer History now uses too.

The visual pass also caught short windows repeating a time label
("Oct 2" four times); the time axis now states each label once.
2026-10-14 12:52:09 +01:00
rcourtman dc92693a2b Stop History chart axis labels crowding or repeating (#2931)
The canvas History chart always drew four time labels, so in a narrow
card, such as a disk's live I/O charts three abreast, they ran together
("12:04 PM12:07 PM12:10 PM12:14 PM"). It now measures its labels and
draws as many evenly spaced times as the plot fits, down to the first
and last.

The drawer charts format sub-kilobyte rates as "<1K/s" on purpose, so a
quiet link printed "<1K/s" on two gridlines, which tells the reader
nothing. Both chart kinds now blank a middle gridline label that repeats
a neighbour; the gridline stays and the end labels always remain.

The range-label test imported two storage modules inside its body and
timed out under load; they are now static imports.


This branch carries the whole drawer chart stack: #2929 (pause hidden
disk charts) and #2930 (keep readings through a resize), plus a receipt
commit. #2929's receipt named its pre-rebase parent, which failed the
range check on both; the last commit records a fresh browser pass of the
final content bound to its own parent. Merging this lands all three.
2026-10-14 12:36:40 +01:00
rcourtman 9c83a43661 Bind the drawer chart stack's browser receipt to its rebased parent
The pause change's receipt named its pre-rebase parent, so the range check could not bind it after #2928 landed. This receipt records the browser pass of the final content of every frontend file the pause, resize and axis commits change, bound to its own parent.
2026-10-14 12:21:20 +01:00
rcourtman ed5914812c Stop History chart axis labels crowding or repeating
The canvas History chart always drew four time labels, so in a narrow
card, such as a disk's live I/O charts three abreast, they ran together
("12:04 PM12:07 PM12:10 PM12:14 PM"). It now measures its labels and
draws as many evenly spaced times as the plot fits, down to the first
and last.

The drawer charts format sub-kilobyte rates as "<1K/s" on purpose, so a
quiet link printed "<1K/s" on two gridlines, which tells the reader
nothing. Both chart kinds now blank a middle gridline label that repeats
a neighbour; the gridline stays and the end labels always remain.

The range-label test imported two storage modules inside its body and
timed out under load; they are now static imports.
2026-10-14 12:20:36 +01:00
rcourtman ef486d54e6 Keep History chart readings through a resize
A History chart sizes its point cap from its width, and the cap was part
of the selection that decides when a chart starts over. Any resize, a
window drag, a drawer or sidebar change, even a full-page screenshot,
cleared the readings and showed a "Loading history" spinner until the
re-read at the new cap returned.

The cap now only shapes the request. The series a chart shows (target,
metric, range, organisation, lock) decides when it starts over, so a
resize re-reads the same series quietly with its readings on screen.
Pointer inspection still resets on a resize, because its pixel positions
moved.

The guest-storage-2263 browser proof now narrows and restores the
viewport and takes a full-page capture with a disk open: the old hook
showed spinners, the new one keeps the charts.
2026-10-14 11:58:46 +01:00
rcourtman c94aec835f Pause a guest's disk charts while Manage hides its Overview
The guest drawer keeps Overview mounted behind the Manage tab so
switching back does not jump. Its own history charts already paused
there, but an expanded physical disk's charts kept polling every 10-30
seconds and the disk list kept refreshing on live updates, all for a
view nobody could see.

HistoryChart now takes enabled: false aborts its read and stops polling
while keeping the readings it shows, and resuming the same selection
reads at once without a loading spinner before polling again. The
drawer's historyEnabled reaches DiskDetail through GuestPhysicalDisks,
which also holds the list's realtime refresh through
useUnifiedResources' realtimeEnabled.

The guest-storage-2263 browser proof now hides and shows the Overview
with the clock running: the old code read disk history while hidden,
the new code reads none and resumes with the same chart nodes.
2026-10-14 11:47:03 +01:00
rcourtman feabe24587 Repair the remaining drawer and storage history browser proofs (#2928)
Sixteen worker-browser proofs still opened the removed History tabs,
clicked the removed Refresh button or depended on UI that has since
moved. Each now passes on pulse-dev:

- Charts are read on Overview (guest, resource, PBS) or inline in disk
  and pool details; table-row-touch toggles Manage/Overview as its
  nested input instead of the old tab.
- Reads are driven through the 30-second poll with the page clock.
- Locators select series paths and hover markers, since bands and
  observation dots also draw paths.
- Rows lifted into Worth checking or kept under the closed Details
  disclosure are read from the whole Overview, with Details opened.
- With stored history shown, the legend reads the latest stored time,
  so series without a point say "no reading" rather than a live value.
- Stubbed history replies echo the requested series and carry min/max,
  as the client has validated since 866da85c26.
- Fixtures that mount the resource drawer supply activeAlerts.
- The guest-stopped-disk write check allows the content-free
  workload-history usage milestone that opening details records.
- On emulated phones, headless Chromium reports a fine pointer, so the
  proof touches the plot before tapping the inspection slider.
2026-10-14 11:46:14 +01:00
rcourtman 8342a35332 Repair the remaining drawer and storage history browser proofs
Sixteen worker-browser proofs still opened the removed History tabs,
clicked the removed Refresh button or depended on UI that has since
moved. Each now passes on pulse-dev:

- Charts are read on Overview (guest, resource, PBS) or inline in disk
  and pool details; table-row-touch toggles Manage/Overview as its
  nested input instead of the old tab.
- Reads are driven through the 30-second poll with the page clock.
- Locators select series paths and hover markers, since bands and
  observation dots also draw paths.
- Rows lifted into Worth checking or kept under the closed Details
  disclosure are read from the whole Overview, with Details opened.
- With stored history shown, the legend reads the latest stored time,
  so series without a point say "no reading" rather than a live value.
- Stubbed history replies echo the requested series and carry min/max,
  as the client has validated since 866da85c26.
- Fixtures that mount the resource drawer supply activeAlerts.
- The guest-stopped-disk write check allows the content-free
  workload-history usage milestone that opening details records.
- On emulated phones, headless Chromium reports a fine pointer, so the
  proof touches the plot before tapping the inspection slider.
2026-10-14 11:27:57 +01:00
rcourtman 0e77bba8d2 Keep Retry focus handoff working in Chromium; repair seven proofs (#2927)
The focus handoff from #2925 tracked Retry focus with focus/blur
handlers. Chromium blurs a focused element when it leaves the DOM, and
the Show branch removes the button before the error effect runs, so the
flag was already false and focus fell to the page. jsdom never blurs on
removal, which is why the unit test passed. Read document.activeElement
when the Retry branch is disposed instead, and make the refresh test
model the removal blur so it fails against the old code.

The worker-browser proofs for history response, pointer, inspection,
window and the three PBS fixtures still clicked the removed History tab
and Refresh button. They now read the charts on Overview, drive reads
through the 30-second poll, select series paths rather than every path
(bands and dots add paths), and mount fixtures in a shared app runtime
(fixtureRuntime.tsx) because drawers read the WebSocket and theme
contexts. The PBS refresh proof is what caught the focus regression.
2026-10-14 11:26:00 +01:00
rcourtman 6f024c1c2d Give the GitHub star prompt a warmer presentation (#2926)
The restored star reminder looked like a routine status notice. The
prompt now uses the earlier “Enjoying Pulse?” heading, a GitHub badge
with a gold star, a colour accent and a full-width action. Its copy asks
for support based on usefulness and discovery.

The existing first-return timing, single lifetime appearance, permanent
dismissal, notice priority and shared actions are preserved. Phone
controls retain 44px touch targets and the card clears the measured
bottom navigation.

Validation: 14 banner tests, frontend type-check and lint, production
frontend and matching mock backend builds, and source-bound Playwright
on /proxmox/overview at 1440×900, 800×900, 390×844 and 320×740 in light
and dark themes. The browser matrix covers wrapping, focus and keyboard
actions, outside click and Escape, new-tab safety, permanent dismissal
and reload, notice priority, transport states and command-palette
suppression.
2026-10-14 11:12:56 +01:00
rcourtman 3a69ad0906 Keep Retry focus handoff working in Chromium; repair seven proofs
The focus handoff from #2925 tracked Retry focus with focus/blur
handlers. Chromium blurs a focused element when it leaves the DOM, and
the Show branch removes the button before the error effect runs, so the
flag was already false and focus fell to the page. jsdom never blurs on
removal, which is why the unit test passed. Read document.activeElement
when the Retry branch is disposed instead, and make the refresh test
model the removal blur so it fails against the old code.

The worker-browser proofs for history response, pointer, inspection,
window and the three PBS fixtures still clicked the removed History tab
and Refresh button. They now read the charts on Overview, drive reads
through the 30-second poll, select series paths rather than every path
(bands and dots add paths), and mount fixtures in a shared app runtime
(fixtureRuntime.tsx) because drawers read the WebSocket and theme
contexts. The PBS refresh proof is what caught the focus regression.
2026-10-14 11:09:55 +01:00
rcourtman 50864566e0 Give the GitHub star prompt a warmer presentation
The restored reminder used a muted compact notice. Bring back the
Enjoying Pulse heading, a GitHub badge with a gold star, a colour accent
and a full-width action. Focus the copy on usefulness and project
support while preserving one-time appearance and permanent dismissal.

Contract-Neutral: Presentation and copy only. Existing notice admission, shared action ownership, destination and persistence contracts are unchanged.
2026-10-14 10:52:53 +01:00
rcourtman b49e5502f9 Drop the history Refresh button and chart only a resource's own alerts (#2925)
I gave Sol (gpt-6.1-sol) a final review of the whole drawer series, with
11 full-resolution screenshots, and checked a real alert on the preview.
This fixes what that turned up and removes the history Refresh button.

**Alert matching**
- A Docker host drew its container's 85% memory trigger on the host's
own memory chart and turned the host reading red. Charts now take
triggers only from `getOwnAlertsForUnifiedResource` (the resource's
exact-key alerts). Needs attention still lists nested alerts.
- A container drawer listed none of its own alerts. The matcher now
answers to `docker:<host>/<container id>` (or `/name:<name>`), the key
`DockerContainerResourceID` writes.
- Docker and resource-detail drawers printed the breach-time alert
message ("memory at 99.5%") beside a chart reading 70%. They now use the
shared `getAlertAttentionCopy`, which reads "Memory 70% now, recovering
/ Clears after 5 minutes at 80% or lower".
- Disk temperature (`temperature`/`diskTemperature` to `smart_temp`) and
storage capacity (`usage`) alerts now reach their charts.

**Charts**
- No alert line is drawn before history exists, where it would sit at a
placeholder height.
- The legend names the trigger ("alert at 85.0%").
- On phones, a panel shown because of an alert stays shown after the
alert clears.
- Facts strips show only the size for retained memory readings.
- The "Refresh history" button is removed; the charts poll every 30s.
"Retry history" appears only for a failed read, and focus moves to the
history region when a successful retry removes it.

**Verification**
- Typecheck plus 826 test files (Workloads, Infrastructure, features,
utils, Storage) on pulse-dev; three docs-rendering tests timed out under
load there and pass on their own. Tests that used Refresh to trigger a
read now advance the poll on a fake interval clock.
- On the built preview, the real alert on container sftp-ingest-2 shows
in its drawer with the dashed line and legend label. The Docker host no
longer draws its container's trigger.

**Not in this PR**
- 23 manual browser proofs still click the removed History tab or
Refresh button; they are fixed in a follow-up.
- Review items deferred, listed for follow-up:
  - nested disk charts in a guest drawer keep polling behind Manage;
  - storage charts lack the band and the phone disclosure;
  - axis-label crowding in four-column layouts;
  - "Latest stored" ignores the response source;
  - inventory cards on resource-detail drawers.
2026-10-14 10:36:49 +01:00
rcourtman 86297b67ea Drop the history Refresh button and chart only a resource's own alerts
A final review of the drawer series (screenshots plus the full diff)
and a real alert on the preview found several problems:

- A Docker host drew its container's 85% memory trigger on the host's
  own memory chart and turned the host reading red. Needs attention
  rightly lists nested alerts; the charts now take triggers only from
  getOwnAlertsForUnifiedResource, the resource's exact-key alerts.
- A container drawer listed none of its own alerts. The matcher now
  answers to docker:<host>/<container id> (or /name:<name>), the key
  the alert engine writes, so it lists them and charts the trigger.
- Docker and resource-detail drawers printed the alert's breach-time
  message ("memory at 99.5%") beside a chart reading 70%. They now use
  the shared attention copy the Proxmox drawers use: "Memory 70% now,
  recovering".
- Disk temperature and storage capacity alerts reached no chart; an
  alert line drawn before any history sat at an arbitrary height; the
  dashed line had no visible label (the legend now says "alert at
  85.0%"); a phone panel shown for an alert hid again when the alert
  cleared, under a reader mid-inspection; facts strips stated a used
  memory amount for a retained reading.

The Refresh history button is gone: the charts poll every 30 seconds.
Retry history still appears for a failed read, and when a successful
retry removes it, focus moves to the history region instead of the
page. Tests that used Refresh to trigger a read now advance the poll on
a fake interval clock.
2026-10-14 10:23:01 +01:00