Commit Graph

5848 Commits

Author SHA1 Message Date
pulse-triage[bot] 53ec5fae93 Integrate reviewed safe Patrol rule-removal guidance
Preserve the exact documentation and regression-test candidate; native browser follow-through and the missing UI remain owned separately.

Change-source: pulse-maintainer
2026-10-05 18:26:38 +01:00
pulse-triage[bot] 377aea8ad5 Integrate reviewed canonical VM Discovery safety repair
Preserve the exact Web candidate and parent-bound browser proof; backend admission and native recovery remain separate work.

Change-source: pulse-maintainer
2026-10-05 18:26:25 +01:00
rcourtman 065cd2bf37 Show Docker container health and uptime in the containers table (#2526)
The Docker containers table now shows whether each container is healthy
and how long it has been running.

- **State names the problem.** It shows Unhealthy (red) or Starting
(amber) for a running container's health check, Exited (139) (red) for a
crash, Restarting, and a muted Exited or Paused for a container stopped
on purpose, with the full sentence on hover. Before, an unhealthy
container read `running` and only its dot was red. The column also
disappeared whenever every container was running, so a fleet with one
failing health check showed no State column at all. The column now
appears whenever any row has one of these states.
- **Stale health is ignored.** Docker keeps the last health status on a
stopped container (#1724), which made a stopped container's dot read
unhealthy. Health now counts only for a running container. A container
with no reported Docker state is treated as running when its status is
online.
- **Uptime column.** From a 900px table, an Uptime column shows the
current run, so `0m` or `5m` marks a container that just restarted. v5
had this column. Stopped containers show a dash, and narrower rows keep
uptime in the drawer.
- **Room for the new columns:**
- A single-host table, which is always flat, drops the Host column that
repeated one name on every row.
- The Ports cell drops the all-interfaces address and the duplicate IPv6
entry, so it reads `23210->8080/tcp`. A specific bind address such as
`127.0.0.1` stays, and the hover text, drawer and search keep the full
form.
  - Column weights moved from CPU, memory, ports and networks.
- New model tests hold the State words (86px) and the Uptime header
(57px) at each layout's narrowest table, alongside the existing
Update-control width test.

Verification:
- Measured every column at nine widths from 390px to 1920px against
main, which now includes #2521. Nothing new clips except one more
truncated container name at 1000px. Ports overflow per cell drops from
175px to 123px at 1440px. "Exited (139)" fits on a 390px phone.
- Checked unhealthy, starting and stale-health states, the single-host
view, dark mode and the phone drawer, using edited API data. The receipt
is included.
2026-10-05 17:37:27 +01:00
rcourtman 2218ab6c18 Keep the Proxmox guest Backup badge whole at laptop widths (#2525)
## Problem

The Proxmox guest table's Backup badge doesn't truncate, but its column
was too narrow for it.

- **Badge widths, with the shield:** Running 73px, Unknown 79px, None
58px, month ages ("12mo") 61px.
- **Column room:** the compact layout gave Backup a weight of 5, which
left a 33–58px content box from 1000 to 1513px. The wide layout gave it
72px.
- **Result:** Running and Unknown were cut inside their pill at every
width up to 1600px, and None and month ages up to about 1500px.

Running is the badge that says a backup is in progress.

## Change

In `guestRowModel.tsx`:

| Layout | Backup | Taken from | Why that column can give |
|---|---|---|---|
| Compact | weight 5 → 10 | Type 7 → 5.4 | It still fits the VM/LXC
badge and the sorted "TYPE▲" header (about 51px). |
| Compact | | CPU 13 → 9.6 | Its bar only shows a percentage and core
count. |
| Wide | 72px → 96px | CPU 140px → 116px | Same reason. |

Name, ID, Uptime, Net I/O, Disk I/O and the memory and disk bars keep
their widths, and their used/total figures. Views without a Backup
column (pods, Docker) give them slightly more, so two
`GuestRow.test.tsx` width expectations change. The peer whose claim
covers that file agreed to the edit; its own PR, #2524, edits a distant
hunk.

## Verification

Mock-mode private stack on main ebd6d09337, before against after, at 15
widths from 390 to 1920px:

- **Badge fit:** every Backup label fits at every width where the column
shows (1000–1920px). Main clipped Running and Unknown everywhere up to
1600px.
- **Clip sweep:** Proxmox goes from 464 to 452 clipped elements and
vSphere from 2 to 1. No column clips more at any width, and the memory
and disk used/total figures and the CPU core counts are unchanged in
every row.
- **Interactions:** clicking the Running badge opens the backup evidence
dialog at 1024, 1280 and 1600px, and Escape closes it. Row details open.
No console errors.
- **Tests:** a new check in
`WorkloadsSurface.performance.contract.test.tsx` covers the narrowest
compact table and the wide width, and fails against main's source. All
94 Workloads test files pass.

The browser receipt is
`frontend-modern/browser-verification/0c24a36bf1559a9d.json`.

**Known residual:** if a user enables an extra compact column, Backup's
share shrinks, and Unknown can clip again at the narrowest compact
width.
2026-10-05 17:19:31 +01:00
rcourtman 51dbf1485a Keep a text-selection drag from toggling table rows (#2524)
Dragging across a pod or container name to copy it ends with a click on
the row the drag stayed inside. Every platform table ran its row action
on that click, so the detail drawer opened or closed and shifted the
layout under the selection.

**Change**

- `TableRow` (`components/shared/Table.tsx`) owns a per-row
`createRowTextSelectionGuard()`. It records where the row was pressed. A
click whose pointer moved more than 3px since that press, while text
inside the row is selected, skips the row action.
- A press released in place still runs the action on the first click,
even over text that is already selected. Chromium clears that selection
only after the click, so a plain selection check alone would need a
second click.
- The 37 `drawer.toggle` tables, the Proxmox tables built on
`getPlatformResourceDetailRowInteractionProps`, and `PulseDataGrid`
inherit the guard with no call-site changes. The custom `GuestRow`
(Workloads) and `StoragePoolRow` shells wire the same guard.
- The disclosure chevron and its keyboard activation handle their own
events and are unchanged. A double-click is two stationary clicks, so it
behaves as before.

**Verification**

- New cases in `Table.test.tsx`, `PulseDataGrid.test.tsx`,
`GuestRow.test.tsx`, `Storage.test.tsx` and
`PlatformResourceDetailTableRow.test.tsx` cover drags, in-place clicks
over a selection, selections outside the row, bound handlers, and the
chevron plus Enter while text stays selected. All six fail with the
guard disabled.
- `src/features src/hooks src/__tests__
src/components/{shared,Workloads,Storage,Infrastructure}`: 580 files and
10,012 tests pass.
- Ran Playwright with real mouse drags on a private mock stack at
1440x900 and 768x1024 across Kubernetes pods, Docker containers, Proxmox
hosts, guests and storage pools, plus 390x844 touch taps.
- The drag reproduces the toggle on the parent commit and leaves state
unchanged on this branch.
  - A plain click on the selected name opens the row first time.
- Receipt: `frontend-modern/browser-verification/4f08442c2617c48d.json`.
2026-10-05 17:19:04 +01:00
pulse-triage[bot] ee618fb993 Guard canonical VM Discovery during guest-read deferrals
Use the existing guest safety signals in the canonical resource drawer, including typed and legacy PVE facets. Keep saved Discovery evidence readable and the backup precaution visible without admitting a manual rescan while paused.

Contract-Neutral: performance-and-scalability; the workload helper input is narrowed without changing its decisions, metrics, render path or performance contract.
Change-source: pulse-maintainer
2026-10-05 17:16:03 +01:00
pulse-triage[bot] 310c7e6e51 Document safe removal of mistakenly created Patrol rules
Explain the missing v6.4.5 undo control and the existing scoped API. Verify exact selection, confirmation, browser session and CSRF, failure handling and readback without copying credentials or deleting unrelated rules.

Contract-Neutral: Documentation and regression-proof coverage of existing Patrol suppression APIs only; no runtime, route, authority or subsystem contract changes.
Change-source: pulse-maintainer
2026-10-05 17:14:39 +01:00
rcourtman d836208f16 Exchange the bootstrap token before any agent module starts (#2520)
## Problem

An agent installed by the cluster deploy flow starts with `--enroll` and
a one-time bootstrap token. Only the host module exchanged that token
for the runtime token. Everything else `main.go` builds at startup kept
the bootstrap token for the whole first process lifetime:

- the remote config client (initial fetch and the one-minute refresh)
- the updater
- the Docker / Podman and Kubernetes modules and their primary report
targets

The bootstrap token carries only the enrollment scope and the server
removes it on exchange, so each of those failed with 401 until the
service restarted. On a freshly deployed Proxmox node this showed up as
a remote config 401 every minute. Those modules also kept the
pre-enrollment agent ID instead of the server-assigned one.

## Fix

`main.go` now enrolls once, before it builds any authenticated client:

- `hostagent.Enroll` is a standalone entry point over the existing
enrollment loop. Payload, headers, retry and backoff, status handling,
and `runtime.token` / `agent-id` persistence are unchanged.
- It runs after self-test and agent-ID preparation and before the first
remote config fetch. Every module is then built from the runtime token
and the canonical agent ID.
- The ID is persisted to the configured agent-ID file, and the host
module's own enrollment is switched off so there is no second exchange.
- A persisted runtime token is reused without a request or host
discovery. A `403` keeps the configured token as a non-bootstrap
credential. A permanent rejection stops startup before any module is
built.
- Windows is gated off and keeps its previous behaviour. Startup there
runs before the service control dispatcher, and an enrollment retry
could outlast its 30-second deadline. No Windows installer passes
`--enroll`.
- Enrollment and reporting now derive hostname, OS and architecture
through one `resolveHostIdentity`, so the runtime token is bound to the
hostname the agent reports under.

## Tests

- `TestRunEnrollsBeforeBuildingAuthenticatedModules`: enrollment happens
before any request to Pulse. Remote config, the updater, and the host,
Docker and Kubernetes configs and their targets all carry the runtime
token and canonical ID, and the agent-ID file is written. With the new
step disabled, this test fails.
- `TestRunStopsBeforeModulesWhenEnrollmentIsRejected`,
`TestRunWithoutEnrollDoesNotExchangeTokens` and
`TestRunLeavesEnrollmentToTheHostModuleWhenGatedOff`.
- `TestEnroll*` cases for `Enroll`: payload and identity, the
monitoring-only command ceiling, reuse of a persisted token (including
when host discovery fails), the 403 keep, the 409 rejection, and a
missing token.
- `TestEnrollPresentsTheIdentityNewReports` checks that enrollment
presents the hostname, OS and architecture that `New` reports, with
Proxmox detection and the architecture fallback.
- `go test ./cmd/pulse-agent ./internal/hostagent
./internal/remoteconfig` passes. `go vet` is clean for linux, windows
and freebsd.

## Not in this change

- The health listener still starts after enrollment, so `/readyz` is
unreachable while enrollment retries on network errors. The installer
only warns in that case. With the health listener enabled, its
registration check now always sees the runtime token. With it disabled,
the process-only check can still race enrollment.
- Distinguishing a deploy binding mismatch from a non-bootstrap token,
which both return 403.
- Two older durability gaps that this change doesn't make worse. If
writing a custom `--agent-id-file` fails, the next start has no fallback
to the canonical ID saved in the state directory. If persisting
`runtime.token` fails, enrollment still succeeds, and the next restart
is left with the spent bootstrap token.
2026-10-05 17:05:17 +01:00
rcourtman 8439dad9e5 Show Docker container health and uptime in the containers table
The State column printed Docker's raw state, so a running container
whose health check failed read "running" and only its dot was red, and
the column hid entirely when every container was running, unhealthy or
not. v5 showed "running (healthy)" and an Uptime column; v6 had lost
both.

State now says what docker ps would flag, from the same verdict as the
row dot: Unhealthy and Starting for a running container's health check,
Exited (139) for a crash, Restarting, and muted Exited or Paused for a
container stopped on purpose, with the full sentence on hover. The
column appears whenever a row has one of those states. A health status
only counts for a running container: Docker keeps the last one on a
stopped container (#1724), which made a stopped container's dot read
unhealthy.

From a 900px table an Uptime column shows the current run, which marks
a container that just restarted; narrower rows keep it in the drawer.
A single-host table drops the Host column that repeated one name on
every row, and the Ports cell drops the all-interfaces address and the
duplicate IPv6 entry, so the widths for State and Uptime come from
columns that had room or truncated anyway.
2026-10-05 16:48:58 +01:00
rcourtman 6fbebfd523 Keep Docker image names readable by their repository and tag (#2521)
## Problem

Image references that share a registry and namespace differ only at the
end, but table cells truncated them from the end:

- **Images (name column):** at 390 and 700px, three rows read
`ghcr.io/pulse-demo…`. From 390 to 900px the repository name was hidden
in 3–4 of 8 rows.
- **Swarm services (Image column):** from 390 to 700px, two of three
rows read `registry.…`.
- **Containers (Image column):** the repository name was hidden in up to
7 of 12 rows at 1024px, and in 3 rows even at 1440px.

So an update badge, a size or a service couldn't be traced to its image
without opening the row. Real fleets are worse: every
`lscr.io/linuxserver/*` image shares a 20-character prefix.

## Change

- **`splitDockerImageReference`** (in `dockerImageReference.ts`) splits
a reference just before its repository name. A digest never moves the
split, and a reference without a path stays whole.
- **`DockerImageReferenceText`** mirrors `KubernetesNameText`: the head
truncates, `name:tag` stays visible, and the full reference stays on the
title.
- **Where it's used:** the Images name cell (via `DockerResourceNameCell
imageReference`), the Containers Image column and the Services Image
column.
- **Copying:** head and tail are flex items, and a browser copy would
join them with a line break. So a selection inside one reference copies
without it; wider selections are left to the browser.
- **Unchanged:** sorting, search and the stored value.

This is a readability fix to existing columns, in the same class as
#2509, #2514 and #2516, so there is no demand-ledger entry.

## Verification

I ran a mock-mode private stack on main 800329cbd8, comparing before and
after at 12–18 widths from 390 to 1600px.

- **Visible text** (each character checked against its clipping box):
  - Rows that read identically fall to 0 on every table at every width.
- Rows hiding the repository name fall to 0–1, and to 0 from 900px on
Images.
- **Clip sweep:**
  - No column other than Image changes, and no metric sublabel is lost.
- Image cells with clipped text are never more numerous than on main at
any width, and fewer at most widths.
- Element counts rise only because a truncated reference now has two
clipping spans; the head truncates by design.
- **Clipboard:** a keyboard copy of one reference reads back
`ghcr.io/pulse-demo/backup-coordinator:2026.04` from
`navigator.clipboard`.
- **Interactions:** sort by Image, and row expand and collapse, work on
all three tables. No console errors.
- **Tests:**
- New tests: the split cases, a rendered Images-table test covering the
tail, the title and the copy scoping, and a source guardrail for the
three image cells.
  - The new and updated tests fail against main's source.
- 544 test files pass across `src/features`, `src/hooks` and
`src/components/{shared,Workloads,Infrastructure}`.

The browser receipt is
`frontend-modern/browser-verification/a68fe9ea6a927e83.json`.

**Known residual:** when the head truncates completely there's no
leading ellipsis (as with `KubernetesNameText`). When it truncates
partly, the gap after its ellipsis can be up to one character wide.
2026-10-05 16:44:03 +01:00
rcourtman 5c452d16ff Merge remote-tracking branch 'origin/main' into fix/workloads-backup-pill-width 2026-10-05 16:42:46 +01:00
rcourtman 3105d5c305 Keep the Proxmox guest Backup badge whole at laptop widths
The guests table's Backup badge does not truncate, but the column was
too narrow for it. Running is 73px with its shield and Unknown 79px.
The compact layout gave Backup a weight of 5 (a 33-58px content box
from 1000 to 1513px) and the wide layout 72px, so Running was cut
inside its pill at every width up to 1600px, and None and month ages
up to about 1500px. Running is the badge that says a backup is
underway.

The compact weights now give Backup 10, taken from Type (5.4, still
room for the VM/LXC badge and the sorted header) and CPU (9.6, whose
bar shows only a percentage and core count). The wide column is 96px,
taken from CPU (116px). Name, ID, Uptime, Net I/O, Disk I/O and the
memory and disk bars keep their widths and used/total sublabels, and
views without a Backup column give them slightly more. Two GuestRow
width expectations for the pod and Docker views move with the CPU
weight.
2026-10-05 16:42:14 +01:00
rcourtman bc5d2ebd64 Copy Kubernetes names without a break between head and tail (#2522)
KubernetesNameText renders a generated name as a truncating head and a
kept tail, and both are flex items. Flex items are blockified, so a
browser copy put a line break between them: copying a pod name gave
"cron-nightly-backfill\n-28918234", and pasting that into kubectl logs
broke. The same happened for the Pods table Node column and controller
names.

A copy whose selection sits inside one name now writes the name without
the break. Wider selections, such as a whole row, still copy as the
browser builds them.
2026-10-05 16:42:07 +01:00
rcourtman 31bcca9519 Keep a text-selection drag from toggling table rows
Dragging across a pod or container name to copy it for kubectl or
docker ends with a click on the row the drag stayed inside. Every
platform table ran its row action on that click, so the detail drawer
opened or closed and shifted the layout under the selection.

The shared TableRow now owns a per-row selection guard: a click whose
pointer moved since the press while text inside the row is selected
skips the row action. A press released in place still runs it on the
first click, even over text that is already selected, because Chromium
clears that selection only after the click. The drawer.toggle tables,
the Proxmox rows built on the row interaction helper and PulseDataGrid
inherit the guard with no call-site changes, and the custom GuestRow
and StoragePoolRow shells wire the same guard. The disclosure chevron
and its keyboard activation handle their own events and are unchanged.
2026-10-05 16:32:10 +01:00
rcourtman ebd6d09337 Install cluster member agents in one step from a covered sibling (#2508)
Adding the Pulse Agent to a Proxmox cluster member meant generating a
token in the UI and pasting it into an interactive prompt on the host,
even though Pulse already knows the cluster and has an agent on its
siblings. The server-side cluster deploy that avoids this was still
shipped, but its wizard was deleted as an orphan in May, and it could
not reach members discovered by hostname: targets were parsed from the
API URL, so a name like "delly2" hit the agent's literal-IP guard and
failed preflight.

Deploy targets now resolve to a literal IP in the member's API URL or
its cluster endpoint's effective IP, matched on the member's own
connection, and members with no known address come back undeployable
with a reason. The Settings > Infrastructure install actions open one
dialog that checks, installs, and confirms each node is reporting,
falling back to the scoped installer when no sibling agent is
connected or a node cannot be reached.

The deploy routes are keyed on the cluster name, so they now refuse a
name shared by two connections instead of merging two sites, and job
creation admits one install per cluster at a time under a lock, which
lets the dialog keep a run going after it is closed.

The agent-lifecycle, api-contracts, frontend-primitives, and
storage-recovery contracts record the new install path, its address
rule, and the persisted, time-bounded admission check.

Supersedes #2507, which split this change across two commits; the
per-commit canonical completion guard needs the contract updates in the
same commit.
2026-10-05 16:15:06 +01:00
rcourtman d7f4f0399d Copy Kubernetes names without a break between head and tail
KubernetesNameText renders a generated name as a truncating head and a
kept tail, and both are flex items. Flex items are blockified, so a
browser copy put a line break between them: copying a pod name gave
"cron-nightly-backfill\n-28918234", and pasting that into kubectl logs
broke. The same happened for the Pods table Node column and controller
names.

A copy whose selection sits inside one name now writes the name without
the break. Wider selections, such as a whole row, still copy as the
browser builds them.
2026-10-05 16:03:58 +01:00
rcourtman 9d4ca81271 Exchange the bootstrap token before any agent module starts
A deployed agent's first run gave the remote config client, the updater, and
the Docker / Podman and Kubernetes modules the bootstrap token, which only the
host module exchanged. Pulse revokes that token on exchange, so those clients
failed with 401 until the service restarted. Enroll once at startup and build
every module from the runtime token and the server-assigned agent ID.
2026-10-05 16:01:15 +01:00
pulse-triage[bot] 834fcaeb36 Preserve guest identity and History consistency with accessible backup evidence (#2518)
## Outcome

This batch makes guest identity persistence safer, keeps broadcast
resource rows and their History targets in one captured generation, and
makes completed-backup evidence accessible by keyboard and touch.
Existing help now explains container CPU scales and passive comparisons.
Native macOS guidance checks receive real GNU timeout without replacing
unrelated system utilities.

The work follows retained identity-write/shutdown failures, fleet CPU
and History consistency evidence, inaccessible backup disclosures and
confusion about container CPU readings. It improves those existing paths
without claiming field recovery or complete performance relief.

## Included reviewed candidates

The complete proposed range is
`800329cbd83c48fdb7402a18633189f2c8a2d6ff..9b057cb141221526a96fac05bdbdf8ed6e8625fc`:

- Guest metadata ordering/coalescing and safe retained shutdown:
`244d322dce` and `5e51069048`, integrated at `d7a305922a`.
- Native guidance prerequisites: `13f6d10798` and `443faa53b6`,
integrated at `f7fca0e164`.
- Accessible existing Workloads backup details: `02e6a90374`, integrated
at `8661c1fc65`.
- Paired complete resource/History capture and faithful full-content
controls: `54de7bdca7`, `32f09dc13d`, `8b12e0ee1b`, `d206b03990` and
`9cc8523881`, integrated at `2787b4eabd`.
- Container CPU documentation: `0c283d2dd5`, integrated at `15e8c5ccf6`.
- Router-owned limiter joins and sealed late worker admission:
`5b560521e2`.

Reviewed identities and authors are unchanged. Current upstream Docker
link editing and platform layout repairs are incorporated by an additive
full-SHA merge; earlier upstream governance and Kubernetes layout
history are retained. The later upstream Docker image badge sizing
repair at `800329cbd83c48fdb7402a18633189f2c8a2d6ff` is also retained
after one successful bounded refresh and additive merge.

## Behaviour and safeguards

Identity polls merge only monitor-owned fields, preserve operator
links/tags/notes and OCI classification, and do not spin-retry
filesystem failures. Incomplete tenant shutdown retains ownership and
prevents directory removal. Router shutdown joins its own limiters
without resetting budgets or stopping another Router.

Broadcast resources and target mappings are detached together. Health
comparison controls use one evaluation instant while production still
samples real UTC at the existing boundary. Full-content, overlay/clear,
identity and observation-time controls remain.

Backup disclosures preserve hover and add keyboard/touch access to the
same read-only evidence. CPU guidance distinguishes host-capacity
normalisation, VM CPU counts, version-qualified rounding, missing
readings and actual alert delivery; it asks for ordinary observations,
not forced workload or threshold changes.

## Validation and remaining limits

- Provenance, retained ancestry, clean additive merge and whitespace
checks pass.
- Every unpublished non-merge commit passes the canonical
completion/history guard. Final-range browser receipt coverage, public
registry audit and contract audit pass.
- Accepted candidate evidence includes focused writer/API races, startup
observations, vet and actual builds; 363 connected backup frontend tests
with four desktop/touch browser cases; CPU help's 132 connected frontend
tests, lint/types/build/budgets, focused Go checks and four Docs browser
cases. Browser receipts cover unchanged final backup bytes and are not
rerun merely because the batch contains several candidates.
- Gate frontend proof at `9daeed13`: 246 tests in four connected files,
type-check, all lint checks, actual embed build and bundle budgets pass;
complete output hash matches and guest stop completes.
- Exact refreshed head `9b057cb1` passes 49 Docker native-table and
platform-layout guardrail tests (`source-l8diazgn`); complete output and
guest stop are verified. This covers the later width-only upstream
change without repeating the earlier full frontend checks.
- Gate backend attempt `source-pz6qwk2t` fails in guest readiness after
capacity waiting, client exit 2, with no command verdict and confirmed
guest stop. It is not a test pass and is not replayed. All backend Go
source, manifests and native workflow bytes are unchanged from reviewed
`5b560521` and its focused races/vet/real-build evidence; that evidence
remains usable without inventing an aggregate pass.
- The retained broad API ten-minute timeout, earlier drain/startup
failures, adverse allocation/frame costs and failed stable RSS retest
are not erased by these focused results. Required exact-head CI remains
authoritative for landing.
- This is not native guest thaw/recovery, PBS badge agreement, CPU/RSS
relief, notification delivery or release inclusion. Independent thaw,
fresh writes on every covered filesystem, liveness and only-prior-active
restoration remain necessary before resuming uncertain monitoring.

Related to #2199
Related to #1723
Related to #2439
Related to #2447


[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-05 15:01:07 +00:00
pulse-triage[bot] e52cf4be50 Incorporate batch-start upstream badge repairs
Change-source: pulse-maintainer
2026-10-05 16:00:31 +01:00
rcourtman ae90e805da Merge remote-tracking branch 'origin/main' into fix/docker-image-name-tail 2026-10-05 16:00:27 +01:00
rcourtman 9ec1503aa2 Keep Docker image names readable by their repository and tag
Image references that share a registry and namespace differ only at
the end, but table cells truncated them from the end. At 390 and 700px
three Images rows read "ghcr.io/pulse-demo…", Swarm services read
"registry.…" from 390 to 700px, and the Containers Image column hid
the repository name up to 1440px, so an update badge or a size could
not be traced to its image without opening the row. Real fleets are
worse: every lscr.io/linuxserver image shares a 20-character prefix.

splitDockerImageReference splits before the repository name (a digest
never moves the split), and DockerImageReferenceText renders the head
as the part that truncates and keeps name:tag visible, mirroring
KubernetesNameText, with the full reference on the title. The Images
name cell, the Containers Image column and the Services Image column
use it. The two halves are flex items, which a browser copy would
join with a line break, so a selection inside one reference copies
without it. Sorting, search and the stored value are unchanged, and a
reference without a path renders as before.
2026-10-05 15:57:33 +01:00
rcourtman a4f88dfd99 List the cluster agent deploy client under both contracts' shared boundaries 2026-10-05 15:37:46 +01:00
rcourtman 8106de0ab0 Keep Docker update and Kubernetes node status badges whole
The Docker container Updates column and the Kubernetes Nodes Status
column hold badges that cannot truncate, and both were cut at the
cell edge at tablet widths.

Docker containers size columns by layout mode with weights that
renormalize over whichever optional columns show (State, Restarts,
Host, Engine). Updates got 54-76px between a 34rem container and the
md breakpoint, where the phone wrap rule no longer applies, so Update
(74px) and Current (69px) were cut from 573 to 800px. The column now
takes its room only from the CPU bar, which never shows a sublabel,
and the collapsed actions menu, so every other column keeps its width
in every layout and column set and the memory bar keeps its used/total
sublabel. The 74px states fit in every layout and column set at the
layout's narrowest table. The 100px states (Check failed, Updating...,
Completed) fit in most layouts but can still clip at a layout's
narrow end when several optional columns show. Wide rows also gain the room their expanded
lifecycle controls need. A model test checks every layout and column
set.

Kubernetes Nodes Status shared the tablet band equally (51px of room
at 573px) and had 8 percent at md (53px at 768px), so Degraded (64px)
was cut. The band now sizes each column for its header and Degraded
at a 34rem table, and md moves 2.5 points from CPU to Status, keeping
the visible total, so Node, Cluster, Roles, Memory and Capacity keep
their widths everywhere. Degraded, NotReady and Unknown fit from
544px; Unschedulable (89px) fits from about 900px.
2026-10-05 15:27:53 +01:00
pulse-triage[bot] 9b057cb141 Retain current upstream Docker badge sizing alongside reviewed maintenance
Incorporate the scoped-read upstream identity through the completed bounded refresh; preserve all reviewed commit identities and existing source evidence.

Change-source: pulse-maintainer
2026-10-05 15:26:24 +01:00
pulse-triage[bot] 9daeed13d8 Incorporate current upstream layout and Docker editing repairs
Preserve the complete reviewed publication batch and upstream commit identities before final-source validation.

Change-source: pulse-maintainer
2026-10-05 15:03:27 +01:00
rcourtman 9874a1c0d5 Merge remote-tracking branch 'origin/main' into fix/docker-images-update-badge 2026-10-05 14:45:01 +01:00
rcourtman dca46d8fca Keep the Docker image update badge readable at tablet widths
The Images table's Update check badge needs about 122px with its cell
padding for its longest labels ("Update available", "Managed by
Pulse"). Between the phone projection and the md breakpoint every
non-name column took an equal 17.5 percent share, which left the
column 100px at a 600px viewport, and the 16 percent md width left it
111px at 768px. The badge was cut at the cell edge with a stray
ellipsis dot, and the "UPDATE CHECK" header clipped at 600px.

The table now declares tablet-band widths through the sortable
header's bandWidth (Update check 23, Size 12; Host and Used by keep
their 17.5) and moves 3 points of md width to Update check, one each
from Host, Used by and Size. The longest size values ("1023 MB",
"9.99 GB") still fit both narrower Size columns.

A clip sweep on a mock stack from 390 to 1440px (15 widths) found the
update badge clipped at 600, 640, 700 and 768px before and nowhere
after. A probe of all six labels against the narrowest Update check
cell found none clipping at any width, and no other column clipped
more often than before. The base already carries the Used by
block-truncate span from #2514, so Used by was measured as it now
renders.
2026-10-05 14:41:49 +01:00
rcourtman 5186d4c560 Bulk-edit Docker container links and hide edits from read-only tokens (#2515)
Follow-up to rcourtman/Pulse#2477.

**Docker containers get the bulk editor**
The strongest demand for saved links (#1556, #624) came from Docker
users, but the editor only reached Proxmox and vSphere guests. The
Docker containers table header now has the same **Edit links** panel,
worded for containers.
- Rows are keyed through `toDiscoveryConfig`, the container drawer's own
metadata target, so bulk and drawer saves read and write the same
`app-container:<host>:name:<name>` record. This includes Docker running
inside Proxmox LXCs.
- The editor now takes a source. Workload tables pass live guests, so
the row model still loads only with the panel. Other tables pass
ready-made rows.
- The Docker table lazy-loads the trigger. Without that, Rollup merged
it into the chunk the Docker and workloads pages share, renamed that
chunk, and broke the `DrawerDiskListCard` bundle budget.

**Read-only tokens no longer see edits that would fail**
Metadata writes require `monitoring:write` for API-token sessions (the
backend's `EnsureScope`). Browser sessions always pass, because metadata
routes have no role check. A kiosk-style `monitoring:read` token saw
edit controls that every save refused with 403. The session store now
mirrors the gate from the security status `tokenScopes`
(`sessionCanWriteMonitoringMetadata()`):
- the bulk editor's **Edit links** button is hidden for such sessions;
- the shared web-interface field shows the saved URL with open and copy,
plus a one-line explanation, but no input, Save, Remove, or "Use this"
suggestion.

**Fewer refetches**
Saves now dispatch their metadata-changed events together after the
writes. `useUnifiedResources` coalesces onto one in-flight refetch, so a
bulk save costs two resource refetches whether it changes 2 links or 4.
Before this, it cost one per link.

**Verification**
- Private mock stack, Docker page and Proxmox page, at 1280 and 375px.
- Real token sessions with the admin bypass off: a `monitoring:read`
token sees no edit affordances, and a `monitoring:read+write` token
saves a container link.
- Receipt: `frontend-modern/browser-verification/e8946b639f30703c.json`.
- Production build: `WorkloadsSurface` 69.08 kB, `Docker` 28.56 kB,
`DrawerDiskListCard` unchanged; no violations.
- Full frontend lint, plus 841 test files (15,618 tests) across
features, hooks, components and stores.
2026-10-05 14:40:18 +01:00
pulse-triage[bot] 5a11eadd41 Reconcile batch-start upstream governance cleanup
Preserve reviewed local integrations and the upstream removal of unused subsystem default proof lists.

Change-source: pulse-maintainer
2026-10-05 14:22:58 +01:00
rcourtman 1358c465f5 Bulk-edit Docker container links and hide edits from read-only tokens
The bulk web-link editor only reached Proxmox and vSphere guests, but the
strongest demand for saved links (#1556, #624) came from Docker users. The
Docker containers table now offers the same Edit links panel, keyed through
the container drawer's own metadata target so bulk and drawer saves share
one record. The trigger is lazy-loaded so it does not merge into the chunk
the Docker and workloads pages already share.

Metadata writes require monitoring:write for API-token sessions, so a
kiosk-style read-only token saw edit controls that every save refused. The
session store now mirrors that gate from the security status token scopes:
the bulk editor is hidden and the shared web-interface field shows the saved
URL without input, Save, Remove, or suggestion adoption. Browser sessions
are unaffected because the backend does not check roles on metadata writes.

Saves now dispatch their metadata-changed events together after the writes,
so a bulk save costs two resource refetches instead of one per link.
2026-10-05 14:12:23 +01:00
rcourtman ff21393c80 Truncate platform summary cells inside their own column
Follow-up to #2509, which fixed the Kubernetes tables. Docker, Proxmox
and vSphere summary cells still wrapped long values in a capped
inline-block span (max-w-48 truncate, max-w-[18rem] truncate, or
truncate inline-block). The cap ignored the column width of these
fixed-layout tables, so a long value ran past a narrower cell with no
ellipsis (a swarm service image by 225px at 700 wide, volume mountpoints
by 51px at 1440), and the inline-block sat about 2px above the row's
text line.

A block truncate span fills the cell, ends in an ellipsis inside it and
sits on the row line. The Docker service Update cell keeps its phone
wrap and truncates inside its column from md instead of at a 10rem cap.

The guardrail from #2509 now covers every platform table source that
carried the pattern, and the coverage table test pins the Node cell.
2026-10-05 14:11:17 +01:00
rcourtman c000972ac9 Require empty subsystem default proof lists the guard never reads
Every subsystem must declare require_explicit_path_policy_coverage, so
build_verification_requirements never falls back to a rule's default
verification policy; an unmatched runtime file becomes a path-policy gap
instead. The default test_prefixes and exact_files were therefore never
consulted, yet 13 of 16 rules carried them (93 files and one prefix),
and authors kept adding proofs there as if they counted.

92 of the 93 files were already proof on some path policy, and
api-contracts' default prefix is on three of its own policies. The one
orphan, internal/api/router_bench_test.go, benchmarks the api-contracts
router rather than any performance-and-scalability runtime, so no policy
should take it; it stays an owned file only.

The registry drops the lists, registry_audit requires them empty and
points authors at the path policy instead, and the canonical protocol
says so. The protocol also stops promising same-subsystem tests "when
the registry explicitly allows them", which the audit no longer permits.

No verdict changes: build_verification_requirements returns identical
requirements for all 2746 owned runtime paths, and for an uncovered
path, under the old and new registry.
2026-10-05 13:50:34 +01:00
pulse-triage[bot] 5b560521e2 Join Router-owned limiter workers during shutdown
Capture all endpoint limiters under their Router owner and seal late worker
admission. Concurrent shutdown joins each cleanup worker without resetting
request budgets or stopping another Router. Integration fixtures use the real
shutdown lifecycle before monitor teardown.

Contract-Neutral: Router worker ownership and concurrent shutdown only; HTTP payloads, limits and windows, auth, tenant selection, revocation and persistence contracts are unchanged.
Change-source: pulse-maintainer
2026-10-05 13:20:08 +01:00
pulse-triage[bot] 2787b4eabd Integrate reviewed same-generation broadcast resource and History capture
Change-source: pulse-maintainer
2026-10-05 13:03:31 +01:00
pulse-triage[bot] 15e8c5ccf6 Integrate reviewed container CPU guidance
Accept exact PI commit 0c283d2dd5 with retained focused source and Docs browser proof. Preserve conditional runtime-host examples and passive comparison safeguards; this is documentation maintenance, not native collection or alert-delivery acceptance.

Change-source: pulse-maintainer
2026-10-05 12:54:47 +01:00
pulse-triage[bot] dfc1b2e8bb Incorporate batch-start upstream Kubernetes layout repair
Preserve the exact upstream history present at coordination start before integrating reviewed CPU help.

Change-source: pulse-maintainer
2026-10-05 12:54:37 +01:00
pulse-triage[bot] 9cc8523881 Compare complete broadcast health at one evaluation instant
The five predeclared diagnostic cases retain a real 1m versus 2m health-age counterexample between separate wall-clock evaluations. Give the independent content oracle and production projection the same private health clock without removing any JSON field; pin minute-boundary age advancement and original LastSeen. Production samples real UTC at the unchanged post-read evaluation point.

Contract-Neutral: Internal health-clock test seam preserves the real UTC evaluation point, public resource/history fields, source precedence, agent identity/lifecycle, tenant/revocation and safety contracts; no wire or permission delta.
Change-source: pulse-maintainer
2026-10-05 12:17:19 +01:00
pulse-triage[bot] 0c283d2dd5 Explain container CPU scales and passive comparisons
Document host-capacity normalisation, version-qualified rounding and the difference between collected readings and delivered alerts. Keep comparisons on ordinary workload observations rather than forced load, threshold changes or diagnostic reruns.

Contract-Neutral: Existing container CPU semantics and safety guidance only; no runtime, API, threshold, permission or lifecycle contract changes.
Change-source: pulse-maintainer
2026-10-05 12:13:26 +01:00
pulse-triage[bot] 8661c1fc65 Integrate reviewed keyboard and touch access to Workloads backup evidence
Preserve the exact Web candidate and its own-parent browser receipt. Retained connected source checks and desktop/touch evidence cover the existing read-only disclosures without changing collection or backup operations.

Change-source: pulse-maintainer
2026-10-05 12:04:21 +01:00
rcourtman b0341768c8 Stop Kubernetes Config and Deployments tables clipping names
Between the phone projection and the md column widths (a 34rem container
below a 768px viewport) the Configuration and Deployments tables split
their width evenly, so names were cut to a few letters beside short
counts. Headers can now declare a percent width for that band through
bandWidth / platformTableBandWidthStyle, and both tables size names
first.

With one cluster in view every scope cell repeated the cluster name and
crowded out the namespace that tells rows apart, so scope cells show the
namespace alone and keep the full cluster/namespace scope on the title
and for assistive technology. Several clusters keep the combined label.

Deployments Desired returns only with wide tables, since Ready already
reads ready over desired, and Ready keeps its denominator until Desired
is visible. On a phone Age takes Ready's spare width so "18h ago" fits.

Summary cells used a capped inline-block span that ignored the column
width: long values spilled past narrow cells with no ellipsis and sat
above the row's text line. A block span fills the cell, ellipsizes there
and aligns with its neighbours.
2026-10-05 12:01:15 +01:00
rcourtman 76b6f7ea97 Install cluster member agents in one step from a covered sibling
Adding the Pulse Agent to a Proxmox cluster member meant generating a
token in the UI and pasting it into an interactive prompt on the host,
even though Pulse already knows the cluster and has an agent on its
siblings. The server-side cluster deploy that avoids this was still
shipped, but its wizard was deleted as an orphan in May, and it could
not reach members discovered by hostname: targets were parsed from the
API URL, so a name like "delly2" hit the agent's literal-IP guard and
failed preflight.

Deploy targets now resolve to a literal IP in the member's API URL or
its cluster endpoint's effective IP, matched on the member's own
connection, and members with no known address come back undeployable
with a reason. The Settings > Infrastructure install actions open one
dialog that checks, installs, and confirms each node is reporting,
falling back to the scoped installer when no sibling agent is
connected or a node cannot be reached.

The deploy routes are keyed on the cluster name, so they now refuse a
name shared by two connections instead of merging two sites, and job
creation admits one install per cluster at a time under a lock, which
lets the dialog keep a run going after it is closed.

The agent-lifecycle, api-contracts, frontend-primitives, and
storage-recovery contracts record the new install path, its address
rule, and the persisted, time-bounded admission check.
2026-10-05 11:43:02 +01:00
pulse-triage[bot] 02e6a90374 Make completed-backup evidence accessible in Workloads
Use native read-only disclosure buttons for both backup entry points, with the shared Dialog preserving named completion evidence, focus and guest row action ownership. Keep existing freshness and uncertainty semantics, collection and backup operations unchanged. Include connected regression tests, the subsystem contract and inspected desktop/phone browser verification.

Change-source: pulse-maintainer
2026-10-05 11:41:55 +01:00
pulse-triage[bot] 54de7bdca7 Capture fresh broadcast resources and history targets together
Resolve canonical source mappings once per complete projection while preserving detached resource content and target selection after host coalescing. Bind resources and targets to the same locked generation, including absence and continuity overlays, and avoid another owned outer-slice copy. No timestamp cache or native CPU/RSS claim. Retain cold, clean and dirty full-content controls and paired complete-projection measurements.

Contract-Neutral: Internal bulk-read and copy-ownership repair preserves existing public resource/history fields, source precedence, agent identity/lifecycle, tenant/revocation and safety contracts; no wire or permission delta.
Change-source: pulse-maintainer
2026-10-05 11:25:57 +01:00
pulse-triage[bot] 76c106c732 Incorporate batch-start upstream main before Delivery review
Change-source: pulse-maintainer
2026-10-05 11:04:58 +01:00
rcourtman 3af37a8862 Stop Kubernetes names clipping at tablet widths (#2506)
Kubernetes names clipped badly between about 600 and 900px wide. At
700px node names read "Pr…" and deployment names two letters.

**Why:** between a 544px table container and a 768px viewport, platform
tables show their phone-hidden columns again, but `md:w-[…]` widths are
viewport media queries and don't apply yet. So every visible column took
an equal share. A seven-column Kubernetes table left its name 83px, with
the chevron and status dot inside it. From 768px the Kubernetes name
widths (14–19%) still cut common node, service and event names.

**Fix:**
- The shared table shell keeps the phone 30% name anchor in that band.
Tables sized by a weighted colgroup are unaffected.
- Kubernetes name columns get room for typical names at 768px. The Nodes
columns are rebalanced so Capacity fits at 1100px and wider.
- Generated names (pods, a pod's node, controllers) truncate their head
and keep the trailing segment that tells siblings apart:
`checkout-a…-c7z2p`, `prod…-02`. The tail never outgrows its cell.
Deployment names keep plain end truncation.

**Measured** with a clip sweep over every Kubernetes tab at 600–1440px:
- Identity-column clips fall from 284 to 186. Most of the remainder are
pod heads truncated by design.
- The Nodes table has no name clipping at any width.
- In the band, TrueNAS Storage and a vSphere resource table go from 14
name clips to 0, with no new clips on other platforms.
2026-10-05 11:00:20 +01:00
pulse-triage[bot] d7a305922a Merge reviewed Core guest metadata writer and shutdown safety
Change-source: pulse-maintainer
2026-10-05 10:56:58 +01:00
rcourtman 1ced3fa995 Stop Kubernetes names clipping at tablet widths
Between a 544px table container and a 768px viewport, platform tables
show their phone-hidden columns again, but md:w widths are viewport media
queries and do not apply yet, so every visible column took an equal share.
A seven-column Kubernetes table left its name 83px, with the chevron and
status dot inside it, so node names read "Pr..." and deployment names two
letters. From 768px the Kubernetes name widths (14 to 19 percent) still
cut common node, service and event names.

The shared table shell now keeps the phone 30 percent name anchor in that
band. Tables sized by a weighted colgroup are unaffected. Kubernetes name
columns get room for typical names at 768px, with the Nodes columns
rebalanced so Capacity fits at 1100px and wider. Generated names (pods,
the node a pod runs on, controllers) truncate their head and keep the
trailing segment that tells siblings apart, and that tail never outgrows
its cell.
2026-10-05 10:29:03 +01:00
rcourtman cb9b3e82bc Retire the same-subsystem test wildcard the guards never honoured
staged_verification_files_for_requirement() matches
allow_same_subsystem_tests against a subsystem's
owned_prefixes/owned_files, but both commit guards passed it the
impacted entry, which carried neither. Since the v6 import (778a2577b6)
a same-subsystem test never satisfied a requirement, while
subsystem_lookup.py (which passes the registry rule) and the guard's own
BLOCKED message ("same-subsystem test/spec files are also accepted")
said it would.

Honouring the 12 flags would have loosened the guard well past their
apparent intent. Every rule requires explicit path-policy coverage, so
the five rule-level flags are never consulted, and on the seven path
policies the flag reaches every test the subsystem owns: a
mutation-registry change would accept any internal/ai or AI Chat test
(~455) instead of its 4 listed proofs, and a frontend Actions change any
Go test under internal/unifiedresources. Six of the seven already list
their own directory in test_prefixes.

So the strictness stays and the tools now agree on it: the registry sets
the 12 flags to false (what has always been enforced), registry_audit
rejects true and points at test_prefixes/exact_files, and the impacted
entry carries the rule's ownership so the completion and commit-shape
guards read the flag the same way subsystem_lookup does.

No requirement accepts a proof it rejected before. subsystem_lookup
stops reporting matches the guard refused: a new internal/hostagent test
no longer claims three agent-lifecycle policies.
2026-10-05 10:27:17 +01:00
pulse-triage[bot] 5e51069048 Allow explicit recovery of retained guest metadata after failed close
Keep admission sealed and reuse a still-running close observer. A later explicit shutdown attempt may persist retained state after a completed write failure; successful close remains idempotent. This avoids a permanent offboarding dead end after an ordinary filesystem repair without an automatic spin retry or overlapping writes.

Contract-Neutral: Internal JSON writer recovery behaviour only; public routes, fields, permissions, identity format, private file mode and atomic persistence are unchanged.
Change-source: pulse-maintainer
2026-10-05 09:31:01 +01:00
pulse-triage[bot] 244d322dce Coalesce guest identity writes and retain incomplete tenant shutdown
Admit detached metadata in order, merge identity fields without restoring stale
operator edits, and persist complete snapshots with one owned writer. Seal late
writes and return shutdown failures before organisation data or offboarding is
removed. Preserve existing drain budgets, file format, tenant boundaries and
atomic persistence. Add controlled burst, ordering, write-error, blocked-close
and real deletion controls.

Contract-Neutral: Behavioural repair of existing JSON persistence and organisation deletion; no route, field, permission, identity format, schema or provider authority change. Shutdown failure uses the existing HTTP error envelope. Internal ownership methods and explanatory contract text change, not the public contract shape.
Change-source: pulse-maintainer
2026-10-05 09:21:40 +01:00