Supersedes #2435, whose branch conflicted with the Tailwind 4 move
(#2443) in `proxmoxHostTableModel.test.ts`. Same change, rebuilt as one
commit on current main with a fresh browser pass and receipt.
- **Coverage:** the Restore column is now Last backup, meaning the
newest completed PBS snapshot or PVE backup file. Guest snapshots no
longer count, matching the posture engine.
- **Phones:** the node, backup server, coverage and By date tables keep
their values whole on phones.
- **By date Details:** shows the archive format (`vma.zst`, `tar.zst`)
with the full volid on hover and in search.
Verified on a private mock stack at 1440, 560, 440 and 390px, in light
and dark. On phones, only identity strings clip.
A second commit restores two `storage-recovery.md` notes from main,
covering History touch inspection and pool-to-disk ownership. The
original branch had dropped them with a stale copy of the file.
Fixes#2350 (from #2118).
The Anthropic client logged `cache_creation_input_tokens` and
`cache_read_input_tokens` and then returned only `input_tokens` and
`output_tokens`, so every cached Patrol run reached the usage ledger,
the cost preview, the budget check and the weekly digest with its cache
buckets missing. #2350 reproduced the loss on both the `Chat` and
`ChatStream` paths.
Both response paths now carry the two buckets as fields disjoint from
`InputTokens` (streaming assigns them from `message_start` and the final
cumulative `message_delta` rather than summing). The agentic loops,
Patrol responses and run records sum them beside the ordinary totals,
the usage ledger and its persisted copies keep them, and
`cost.EstimateUsageUSD` prices each bucket at Anthropic's published
cache rates (5-minute write 1.25x, read 0.1x of the input rate), falling
back to the input rate for providers without a known cache price. Spend
summaries, the digest spend, the cost projection medians and the usage
export expose the buckets and price them the same way. `InputTokens`
keeps its meaning everywhere, so the telemetry token buckets are
unchanged.
Proof: `anthropic_test.go` (both response paths keep the buckets; the
final delta does not double them), `pricing_anthropic_test.go`,
`store_test.go`, `patrol_digest_test.go`,
`patrol_cost_projection_test.go`, `cost_persistence_test.go`, and
`ai_handlers_more_test.go` for the export in both formats. This repairs
lost accounting data; it is not a measured provider bill or a claim of
savings across six-hourly runs.
Supersedes #2432, whose branch conflicted with the Tailwind 4 move
(#2443) in `StackedDiskBar.tsx` and `DockerTasksTable.tsx`. Same eight
fixes, rebuilt as one commit on current main. The conflict resolution
keeps both this change and main's class renames (`rounded-sm` to
`rounded-xs`). A fresh browser pass and receipt are included.
Proxmox
- PBS datastores read Online in the storage vocabulary instead of
Unknown.
- The mock estate carries shared PBS storage as one cluster row.
- Storage types read LVM-Thin and NFS, and cluster-merged storage reads
"Shared · N nodes".
- REST-mapped resources carry PMG metadata, so the Mail Gateway table no
longer paints dashes on fresh loads.
- In Bars mode, multi-disk hosts show their fullest disk's percentage.
- REST rows carry source status, action readiness and technology.
Docker
- The containers table fits at laptop and phone widths.
- Swarm states read as words, and task times show as ages.
- Demo container names are unique per host.
Verified on a private mock stack built from this tree at 1440, 1280 and
390px, in light and dark.
## Why
The Workload Controllers table hides its Detail column below a 76rem
container, and that column was the only place a Job's completion time
and a CronJob's last success or last run were shown. The row expansion
rendered only the generic Observed state, Last seen, Identity and
Aliases, so a phone or a half-width pane had no way to read when a job
finished. At 390px the 15 percent Kind track also clipped "DaemonSet",
"StatefulSet" and "CronJob", and the Target track clipped "1
completions".
## What
- The drawer's Kubernetes model builds a per-kind controller section
(Job, CronJob, DaemonSet, StatefulSet, ReplicaSet) that the
always-visible summary tables render in the same slot as the Docker
container section, so the facts are one tap away at every width and on
the Infrastructure drawer too. Job rows lead with Started, Completed and
Duration; CronJob rows with Schedule, Last run and Last success; each
timestamp is the absolute time with its age in parentheses and wraps in
a phone-width cell. Target, counts, the StatefulSet service name,
Namespace and Cluster follow.
- The phone projection keeps Controller, Kind, Ready and Issues and
demotes Target with `platform-table-phone-hidden`, widening Kind to 25
percent so every kind label fits whole.
- The Target count pluralizes through `getPlatformResourceCountNoun`, so
a single completion reads "1 completion" in the row as it does in the
expansion.
## Verification
Playwright clip sweep (scrollWidth over clientWidth on every th/td and
their children) at 1440, 1280, 900 and 390px against a private mock-mode
stack, with each CronJob, Job, DaemonSet and StatefulSet row expanded at
every width:
- 390px: the Kind track is 90px and no kind label or target value clips;
only the four long controller names truncate with an ellipsis. The
wrapped timestamp values in the expansion no longer clip (a single-line
value had needed 169px in a 142px cell).
- 1440px: no new clipped cell; the StatefulSet service name, which the
Detail column truncates, now reads whole in the expansion.
- No viewport scrolls horizontally.
Unit tests cover the controller section per kind and the table's phone
classes and expansion content.
`frontend-modern/browser-verification.json` carries the receipt.
Builds on #2411. Supersedes #2417, which conflicted with main on the
shared browser-verification receipt.
## What was wrong
The metric, stacked memory and stacked disk bars choose between `60%`
and `60% (265 GB/440 GB)` by estimating the label's width. The estimate
was 5.5px per character plus 8. At the label's 10px type a digit is
6.7px wide and a full stop 3px, so the estimate was off by -7px to +5px
on real labels.
In the Kubernetes clusters table at a 560px viewport the memory bar is
113px wide, the estimate came to 112.5px, the label needs 118px, and it
rendered with an ellipsis. A sweep of the platform tables found 24 such
bars on `main`, all stacked memory bars: 3 on Kubernetes clusters at
560px, 4 on Proxmox overview at 900px, 16 on Docker overview and 1 on
Kubernetes nodes at 1440px.
## The fix
`estimateTextWidth` sums per-glyph advances instead of multiplying a
character count.
- It reads the advances once per page from a hidden probe that carries
the label's type classes, so the estimate follows whichever system font
the browser renders. The advances measured for the macOS system font
stand in where there is no layout (unit tests).
- The value is measured semibold and the detail after it at normal
weight.
- Each model adds the padding its own label sits in (4px in the metric
and disk bars, 8px in the memory bar's chip) instead of sharing one
constant.
Two more gaps in the same decision are closed:
- The stacked memory and disk bars now count the anomaly marker that
shares the label's line.
- The inline per-disk slots are sized after the gap between them. A
two-disk Proxmox node at 900px clipped `backup 80%` by 2px.
A flat safety margin was not enough: the error depends on which glyphs
the label holds, so a margin that stops the clipping also hides labels
that fit. Measuring each label in the DOM would add a layout read per
bar on the Workloads hot path.
## Verification
Playwright sweep of every table on 32 platform routes at 1440, 900, 700,
560 and 390px (160 loads, 2,579 bars), on this branch and on `main`
against the same mock backend:
| | `main` | this branch |
| --- | --- | --- |
| Bars whose chosen label does not fit | 24 | 0 |
| Clipped inline disk labels | 3 | 0 |
| Bars showing the long label | 567 | 550 |
Of the 17 fewer long labels, 24 are the clipped ones, 14 Proxmox guest
disk bars at 900px gained the long form, and 7 memory bars dropped it: 5
that had 1.0px to 2.8px to spare and 2 that overflowed by under 0.5px.
With every glyph forced to Verdana (10% wider) no bar clips; with Arial
(7% narrower) no bar clips and 237 more show the long label. WebKit
matches Chromium.
The probe is the estimator's only layout read: 1.7ms on the 429-bar
Proxmox overview, about 110ns per estimate afterwards. Bar widths still
come from the resize observer alone.
## Not changed here
The storage usage bar on Proxmox > Storage still clips at 390px (72
rows) and by up to 2.3px at 700px (19 rows). It is a different component
that always renders the long label and does not use the estimator; #2418
reworks it.
The original PR 2435 branch carried a stale copy of storage-recovery.md and deleted main's History touch-inspection and pool-to-disk ownership notes, which are unrelated to this change. The squashed rebuild inherited the deletion; this puts both notes back so the change only adds its own coverage clause.
PR #2445 and this branch both appended a section to the end of api-contracts.md. Adding main's section here, ahead of this branch's, lets origin/main merge without a conflict the commit hooks cannot conclude; the merged file keeps both notes.
Rebuilds #2432 as one commit on current main. Its branch conflicted
with the Tailwind 4 move (#2443) in StackedDiskBar.tsx and
DockerTasksTable.tsx; the resolution keeps this change and main's class
renames (rounded-sm to rounded-xs). The eight original commits' reasons
follow.
Read PBS datastore status in the storage vocabulary:
The PBS poller reports a datastore as "available", or "unavailable"
when PBS returns an error for it. The datastore adapter mapped that
string through the generic status mapper, which only knows online,
offline and warning, so every healthy PBS datastore showed as
"Unknown" in the Storage tab and an unavailable one was not flagged.
PVE storage already maps the same vocabulary through its own storage
mapper. Share that mapping so a PBS datastore reads online or offline
like any other storage.
Carry shared PBS storage as one cluster row in the mock estate:
The mock emitted each cluster's two shared PBS storages once per node,
each with its own usage figure. The real poller folds a shared storage
into one cluster-scoped entry, which users asked for in issue 1049, so
the demo's Storage tab contradicted the shipped behaviour: ten rows of
"west-backup-primary" with different usage and growth on a five-node
estate, and a hundred on the public demo.
The fixture now carries the merged shape: one row per store with the
reporting nodes listed and a cluster-scoped id, offline only when no
cluster node is online.
Name Proxmox storage types and shared hosts as operators know them:
The Storage tab title-cased any storage type it did not map, so the
default local-lvm on every Proxmox install read "Lvmthin" and an NFS
share read "Nfs". Those types now carry the names the Proxmox UI uses:
LVM-Thin, LVM, NFS, SMB/CIFS, iSCSI, ZFS, BTRFS, GlusterFS and ESXi.
A storage the poller merged across the cluster showed the synthetic
node marker "cluster" as its host. It now reads "Shared", with the
reporting node count when the storage lists its nodes.
Carry PMG metadata on REST-mapped resources:
The Mail Gateway table reads resource.pmg. The REST-to-Resource mapper
set every other provider facet at the top level but kept pmg only under
platformData, so a row sourced from the REST snapshot rendered every
PMG column (version, nodes, mail in, spam, virus, quarantine, queue,
deferred) as a dash. The websocket snapshot carries pmg at the top
level, so the table filled in only when a websocket row happened to
replace the REST row; on a fresh load the two transports race, and
about half the loads stayed blank for good.
The mapper now carries pmg like the other facets, and a test pins it.
Label multi-disk hosts with their fullest disk in Bars mode:
A host with more than one agent-reported disk draws its Disk cell as
one micro-bar per disk and no figure, while a single-disk host shows a
percentage and Trends mode shows one for every host. Bars is the
default view, and most Proxmox nodes have at least local-zfs beside a
data pool, so the question "which node is running out of space?" had
no at-a-glance answer for the rows that matter most; the column's own
sort key was a number the cell never showed.
The cell now carries the fullest disk's usage beside the micro-bars.
Every bar keeps equal weight and no disk is named in the row; the
mount and the per-disk breakdown stay in the title and the tooltip.
Carry source status, action readiness and technology on REST rows:
The same race that blanked the Mail Gateway table applies to three
more fields the API returns: sourceStatus, actionReadiness and
technology. The REST mapper set sourceStatus only under platformData
and dropped the other two, while websocket rows carry all three at the
top level. Docker container lifecycle actions read sourceStatus and
actionReadiness to decide refusals, storage rows read sourceStatus for
freshness, and search suggestions and the assistant context read
technology, so whichever transport won a fresh load decided whether
those consumers saw the data.
The mapper now carries all three, the REST type declares the two it
omitted, and the PMG mapping test pins them.
Fit the Docker containers table and read Swarm state as words:
On a 1280-1536px laptop window the containers table clipped its short
values: the engine read "docker 2…", host names lost their tail, and
the Update control itself was cut off, while the percentage-only CPU
and memory bars had room to spare. The compact widths now give those
columns their share. On a phone the row dropped to "runni…" with an
unreadable Restarts header beside a wrapped Update button; the phone
row now keeps container, state, CPU, memory and Update, and the
restart count stays in the row expansion until mobile width. The
images table on a phone showed "Not c…" for the update verdict and a
clipped host; it keeps image, size and update check there and defers
host and consumers to the expansion.
Swarm services showed the rollout state as the raw token
"rollback_started" and Swarm tasks showed a raw RFC3339 stamp with
microseconds as their start time. The state reads as words and the
start as an age, with the raw values in the title.
Keep demo Docker container names unique per host:
The demo scenario reused its curated container profiles for a second
round on hosts with more containers than profiles, so a host showed
two running "customer-portal" rows and every image's "used by" list
read "customer-portal, customer-portal". Docker refuses two containers
with one name on a host, so the duplicates read as a bug to anyone
evaluating the demo. A reused profile now takes a numbered suffix.
Contract-Neutral: Storage type labels, shared-host label, REST mapper carrying fields websocket rows already carry, Bars-mode disk label and Docker column fits are presentation and mapping only; owning unified-resources and storage-recovery clauses updated; no frontend-primitives primitive, API or runtime boundary change
Rebuilds #2435 as one commit on current main. Its branch conflicted with
the Tailwind 4 move (#2443) in a host-table test; the three original
commits' reasons follow.
Name the last backup in Proxmox coverage, not the newest snapshot:
The Coverage table's Restore column showed the newest of every artifact,
including guest snapshots. A VM with a snapshot from last night and a
PBS backup from three weeks ago read "Restore 18h ago" in the healthy
green beside a "Needs attention" posture, and the only way to see why
was the row's hover text. Guest snapshots share the guest's storage;
the posture engine already refuses to count them ("snapshots alone do
not prove independent recovery"), so the row contradicted the server.
The column is now Last backup: the newest completed PBS snapshot or PVE
backup file. Snapshot-only rows show None and carry the engine's own
reason on hover, the guest snapshot keeps its dedicated column, and the
sort and the backup-location filter follow the same rule. The location
filter also stops treating running or failed artifacts as a latest
pointer, which the unfiltered model already did.
Keep Proxmox backup and node tables whole on phones:
On a 390px phone the Proxmox tables clipped the values users came for.
The nodes table showed six "Anal…" rows for the Analytics A–F cluster:
a sixth column (temperature) left the node name four characters wide
with the toggle and status dot in front of it. The backup server row
showed "H…" for Healthy, and the coverage and By date rows clipped
"18h ago" to "18 …" and "Running" to "Runni…".
Nodes now carry the same five columns as the guest table beneath them
with a wider identity cell; temperature returns from 440px, where it
fits. Backup ages use a suffix-free compact form ("18h") with the full
timestamp on hover. The server row keeps its status word and used
percentage whole and lets the backup count return at the next width,
and its stale header width classes no longer fight the colgroup. The
By date row keeps its source and state badges whole while the
repository moves to the row's hover title. Just above the phone
projection the servers' used/total pair and the coverage posture word
now fit, and at desktop width "Automation Lab Standalone" no longer
truncates in the nodes table.
Measured with a scrollWidth-over-clientWidth sweep of every table cell
at 1440px, 560px and 390px; only identity strings still clip, each with
its full text on hover.
Name the archive format in By date and widen phone guest names:
The By date table's Details cell showed a PVE backup's full volid,
"backup-vault:backup/vzdump-qemu-1002-2026_10_02-20_10_14.vma.zst",
which repeats the storage, guest type, VMID, and timestamp the row
already shows in their own columns and clipped in every one of its
rows on a 1440px screen. The one fact it adds is the archive format,
so the cell now reads "vma.zst" or "tar.zst", the volid stays on
hover and in search, and a provider with no format falls back to the
file name. The coverage disclosure's evidence table follows suit.
The shared guest table's phone name cell carries the expansion
toggle, status dot, and backup badge before the name, so the shared
30% identity anchor left a 390px phone 66px for the name and clipped
"artifact-cache-310" to "artifact-ca…". It now takes the same 38%
anchor the Proxmox host table uses, which gives the name 95px while
the four metric chips beside it still fit.
Measured with a scrollWidth-over-clientWidth sweep at 1440px, 560px
and 390px: the By date table has no clipped cell at desktop width, and
on phones only long identity strings still clip.
Contract-Neutral: Proxmox coverage, phone and mid-width column projections, By date Details and guest-table phone identity width are presentation only; owning unified-resources and storage-recovery notes updated; no workloads hot-path or frontend-primitives change
The attention detail header carries Back, Previous and Next issue on
phones and was authored sticky, but the inbox section's overflow-hidden
made that section the sticky scroll container. Below lg the header moved
exactly with the section, so on long decisions the queue controls scrolled
away. overflow-clip keeps the rounded-corner clipping without creating a
scroll container, so the header now pins against the app scroll shell.
The header also gets the opaque bg-surface/95 it was first authored with.
PR 2443 dropped it because Tailwind 3 never rendered it; now that the
header actually sticks, content would otherwise show through it.
Contract-Neutral: Merge of origin/main into this branch: brings in commits already landed on main and deletes the retired shared receipt, no new contract change
# Conflicts:
# frontend-modern/browser-verification.json
GHSA-vfj7-8cjw-p6xm affects every braces release and has no fix, so the
required frontend audit failed with seven high findings on any pull
request that changes the dependency graph. Tailwind CSS 3 (chokidar,
fast-glob, micromatch) and jscpd 4 (@jscpd/finder, fast-glob) were the
only paths in, both dev-only, and no override can remove braces from
fast-glob or micromatch. Tailwind CSS 4.3.3 through @tailwindcss/vite and
jscpd 5.4.0 leave none; npm audit over the full graph reports zero.
The stylesheet keeps the v3 rendering. It pins the v3 palette (inlined,
so opacity modifiers compile to static colors), font stacks and line
heights, keeps v3's cascade order by placing former component and
unlayered rules around the generated utilities, and restores v3 hover on
touch devices, gray-200 default borders, the 3px blue ring, placeholder
color, button cursor, table-cell padding and the space-* and divide-*
child selectors. The 900/950 shades Pulse overrode with 25% alpha are
opaque now and their callers say /25, because v4 multiplies an opacity
modifier into a translucent color instead of replacing it. Opacity
modifiers on var() colors that v3 never generated are removed.
Utility order changed from lexical to numeric, so a few caller overrides
that silently lost in v3 would start to win. Touch targets keep their
authored 44px phone size (the Machines row stays 32px); cosmetic
overrides v3 never applied are removed, and overrides it did apply use
the important suffix.
Contract-Neutral: Tailwind CSS 4 migration for GHSA-vfj7-8cjw-p6xm: utility class renames and stylesheet compatibility, no subsystem contract change
The metric, stacked memory and stacked disk bars choose between "60%"
and "60% (265 GB/440 GB)" by estimating the label's width. The estimate
was 5.5px per character plus 8, which is off by -7px to +5px on real
labels: at the label's 10px type a digit is 6.7px wide and a full stop
3px. In the Kubernetes clusters table at a 560px viewport the memory
bar is 113px wide, the estimate came to 112.5px, the label needs 118px,
and it rendered with an ellipsis. A sweep of the platform tables found
24 such bars on main, all stacked memory bars, at 560, 900 and 1440px.
estimateTextWidth now sums per-glyph advances. It reads them once per
page from a hidden probe that carries the label's type classes, so the
estimate follows whichever system font the browser renders; the
advances measured for the macOS system font stand in where there is no
layout. The value is measured semibold and the detail at normal weight,
and each model adds the padding its own label sits in instead of
sharing one constant.
Two more gaps in the same decision are closed. The stacked bars now
count the anomaly marker that shares the label's line, and the inline
per-disk slots are sized after the gap between them: a two-disk Proxmox
node at 900px clipped "backup 80%" by 2px.
The probe is the estimator's only layout read, 1.7ms on the 429-bar
Proxmox overview, and never per bar. Bar widths still come from the
resize observer alone.
The per-commit receipt change left
frontend-modern/browser-verification.json in the tree, frozen, so that
it would not conflict with the pull requests still rewriting it. Nothing
reads the file at the tip: range mode reads a receipt from the commit
that recorded it. Left in place it invites the old habit and keeps those
pull requests conflicting with each other on it.
The guard now refuses a staged tree or a range tip that carries the
file, rather than only a commit that writes it. Without that, resolving
the modify/delete conflict by keeping the file changes nothing against
HEAD, passes the hook, and restores the file on main when the branch
merges.
A pull request that still rewrites the file is repaired by merging main
and removing the file with git rm. Its commits remain valid evidence in
range mode.
Every commit touching user-visible frontend source rewrote the single
frontend-modern/browser-verification.json, so any two open frontend pull
requests conflicted as soon as either merged. A merge could not repair
the loser: concluding it staged main's frontend files as if the author
had changed them, and the guard then demanded a receipt for files the
author never ran. The only honest recovery was a rebuild on the new main
and a fresh browser pass, as #2415 -> #2418 -> #2433 showed on 2-3
October.
Each commit now adds its own receipt under
frontend-modern/browser-verification/, named by the fingerprint of the
content it verified. Receipts are only ever added, so git merges them
unaided. The guard keeps every binding it had: the commit's own parent,
the exact changed paths and their content digests, a desktop and a
narrow viewport, non-empty routes, states and interactions, and the
range check that each changed file's final content matches a receipt
from a non-merge commit in the range.
Concluding a merge no longer counts content a merged parent already
carries as the commit's own change. Merged content that neither parent
holds is flagged and still needs a follow-up receipt in range mode, and
a merge commit may not record one.
The shared file is retired: no commit may write it again. A commit that
modified it while its own parent still carried it stays valid evidence
in range mode, so a pull request that still rewrites it is repaired by
merging main and taking main's copy rather than by a rebuild. The file
is deleted in a change of its own, because deleting it here would
conflict with every pull request that still rewrites it.
--write-template writes the skeleton at the path the staged content
requires. --prune deletes receipts already on main. A change that
records a receipt may not delete one: git pairs the two as a rename,
which conflicts with any other change deleting the same receipt.
The Workload Controllers table hides its Detail column below a 76rem
container, and that column was the only place a Job's completion time and
a CronJob's last success or last run were shown. The row expansion rendered
only the generic Observed state, Last seen, Identity and Aliases, so a phone
or a half-width pane had no way to read when a job finished. At 390px the
15 percent Kind track also clipped "DaemonSet", "StatefulSet" and
"CronJob", and the Target track clipped "1 completions".
The drawer's Kubernetes model now builds a per-kind controller section
(Job, CronJob, DaemonSet, StatefulSet, ReplicaSet) that the always-visible
summary tables render in the same slot as the Docker container section, so
the facts are one tap away in every width and on the Infrastructure drawer
too. Job rows lead with Started, Completed and Duration; CronJob rows with
Schedule, Last run and Last success; each timestamp is the absolute time
with its age in parentheses and wraps in a phone-width cell. Target, the
counts, the StatefulSet service name, Namespace and Cluster follow, which
also shows the service name the Detail column truncates at 1440px.
The phone projection keeps Controller, Kind, Ready and Issues and demotes
Target with platform-table-phone-hidden, widening Kind to 25 percent so
every kind label fits whole. The Target count now pluralizes through
getPlatformResourceCountNoun, so a single completion reads "1 completion"
in the row as it does in the expansion.
Verified with a Playwright clip sweep at 1440, 1280, 900 and 390px against
a private mock-mode stack; the receipt records the measurements.
Contract-Neutral: Kubernetes controller facts move into the existing drawer summary slot and the phone row uses the existing platform-table-phone-hidden marker; owning unified-resources clause updated; no frontend-primitives clause, primitive, or API change
The Anthropic client logged cache_creation_input_tokens and
cache_read_input_tokens and then returned only input_tokens and
output_tokens, so every cached Patrol run reached the usage ledger, the
cost preview, the budget check and the weekly digest with its cache
buckets missing. Pulse #2350 reproduced the loss on both the Chat and
ChatStream paths, and #2118's reporter was told to trust the provider's
billing view over Pulse's totals until this was fixed.
Both response paths now carry the two buckets as fields disjoint from
InputTokens (streaming assigns them from message_start and the final
cumulative message_delta rather than summing). The agentic loops, Patrol
responses and run records sum them beside the ordinary totals, the usage
ledger and its persisted copies keep them, and cost.EstimateUsageUSD
prices each bucket at Anthropic's published cache rates (5-minute write
1.25x, read 0.1x of the input rate), falling back to the input rate for
providers without a known cache price. Spend summaries, the digest spend,
the cost projection medians and the usage export expose the buckets and
price them the same way. InputTokens keeps its meaning everywhere, so the
telemetry token buckets are unchanged.
## What changed and why
Alert cleanup now retires an expired flapping episode together with its
suppression deadline. Previously either cleanup owner could remove the
deadline but leave the episode latched, suppressing notifications
without a newly bounded cooldown. Active alert identity, unexpired
suppression and ordinary repeat-notification cooldown remain unchanged.
TrueNAS troubleshooting now distinguishes stale polling, missing
telemetry, authentication failures and session turnover. The existing
guides preserve polling evidence before a manual connection test updates
timestamps, use the actual 60-second default and avoid credential resets
or destructive reproductions. This addresses the diagnostic gaps exposed
by #2382, #2396 and #2400; it does not claim those native incidents are
all resolved.
## Reviewed source included
Complete unpublished range:
`0e90236d3bb901dc84d4d07e70d4d2a09d5e6705..971ebde8afdc0f55dd0ea77a0358467caf62ea73`
(five commits, including additive integration history).
- TrueNAS guide candidate `4b0f23e0a7a7d03df391f83896cbf4b48657bd24` (2
October).
- Alert cleanup candidate `d813c936e464eb09bb9ae7e6c7ac33716acde378` (2
October).
- Integration tips `e9294420`, `71382de0`, `971ebde8` preserve the
original candidates and published main ancestry without rewriting them.
## Validation
- Accepted alert candidate evidence: six parent-failing cleanup cases
pass on final; full alerts correctness/races, ten focused race repeats,
notification correctness, library vet/build and 132 governance checks
pass. The integrated alert files are identical to that candidate. These
proofs used Go 1.26.7; declared Go 1.26.8 CI remains required.
- Accepted guide evidence: seven guide checks, three polling controls
and 25 Docs/viewer tests; desktop Chromium and phone WebKit
navigation/readability checks. Integrated guide/test files are unchanged
from that candidate.
- Exact combined head: seven guide checks pass in offline source proof
`source-x4fm8fxy`, exit 0, complete output and completed guest shutdown.
The preceding confined lightweight attempt could not create loopback
sockets (five errors); it is an execution-context limitation, not a
pass.
- Publication provenance and whitespace checks pass; live main matches
the range base.
Required exact-head CI and independent source review still gate landing.
Real-duration alert queue/HTTP acceptance remains with its existing
owner after an interrupted proof; native monitoring acceptance and
containing-release delivery remain separate. No release, tag or
deployment is changed by this PR.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
TableCell and TableHead composed `px-2 sm:px-3 py-*` ahead of the
caller's class, but Tailwind emits every padding utility at the same
specificity in spacing-scale order, so a caller's `px-1` lost to the
base `px-2` and `sm:px-1.5` lost to `sm:px-3` wherever it sat in the
string. Measured on 2026-10-02: DiskList cells carried 16px of
horizontal padding on a 390px phone although their class asked for
8px, and 24px at 640-1023px although it asked for 12px; the Workloads
header cells sat at 8/12px above data cells at 6/8px; the platform
helper's `px-1.5 sm:px-2` had never taken effect on a cell since it
was introduced in May. PR #2409 and PR #2418 worked around it with
layout-mode `!px-1` overrides.
Apply the convention TableHeader already uses for borders and
TableBody for dividers to padding: when the caller's class names a
side's padding (`p-*`, `px-*`, `pl-*`, `pr-*`, `py-*`, `pt-*`, `pb-*`,
with or without `!`), the base padding for that side is left out, so
the caller's utility is the only one on the element. Sides the caller
leaves alone keep the base, so a `pr-2` column still gets its left
inset, and prefixed-only variants such as `lg:px-0` keep layering on
the base as before. The base is unchanged when nothing is passed, and
the class is resolved inside the JSX attribute so layout-mode class
switches stay reactive.
Platform tables pick up the helper's declared 6/8px cell padding,
which lines data cells up under the 6px headers the stylesheet already
pins, and their header rows drop to the helper's 2px vertical padding.
Both cleanup owners removed only the deadline, leaving a latched episode that could suppress notifications without a new cooldown. Share dispatch expiry with cleanup, preserve unexpired policies and active occurrence identity, and verify renewed delivery, bounded rearming, diagnosis and one-shot callbacks without bypassing ordinary repeat-notification cooldown.
Change-source: pulse-maintainer
Preserve existing polling evidence before a connection test updates its ledger. Explain freshness, optional telemetry and session turnover using the read-only projection, correct the 60-second default, and keep diagnostic steps free of credentials and destructive reproductions.
Contract-Neutral: Documentation and existing-guide checks only; no runtime, API, transport, identity, access or retention change.
Change-source: pulse-maintainer
Retain both History/storage and Reporting contracts; keep the upstream browser receipt unchanged. Earlier accepted receipts remain in history, with runtime blobs preserved.
Change-source: pulse-maintainer
Recognise matching notify_unsubscribed events before and after acknowledgement, preserve unrelated streams and clean log completion, and retain only numeric errno with fixed diagnostics. Keep nonempty Apps inventory and poll-ledger progress while rejected stats recover on the next ordinary poll. The reporter supplied a source-derived envelope, not a retained capture; native acceptance and delivery remain separate.
Change-source: pulse-maintainer
The patrol_digest report schedule kind has been on main since early
September, but only the API could create one: the Settings > Reporting
form had no way to choose it, so the weekly "what Patrol did" email never
reached the customers it was built for, the ones who have stopped opening
Pulse. The schedule form now has a Report type choice between the
performance report and the Patrol weekly summary. The choice shows only
while the session reports Patrol can run (AI on with a provider), and an
existing summary schedule keeps showing its type so it stays editable
after AI is switched off.
Choosing the summary reduces the form to a name, weekday, time, timezone
and optional recipients. Cadence is pinned to weekly and delivery to
email, and the format, resource picker, tag filter, retention, attach and
save-copy controls are hidden because the server fixes those values; the
save path no longer demands a resource scope for this kind. The schedules
table describes a summary as "Patrol activity, last 7 days". Model, hook
and panel tests cover the gating, the pinned fields and the saved
payload, and the browser receipt records a Pro-licensed pass at 1280 and
375 pixels, including a run without an email destination that surfaces
the server's message naming the missing setting.
The existing drawer fetches all metrics without a metric query field. Explain metrics-object and single-metric points responses so readers do not wait for an absent CPU request or mistake missing CPU and measured zero for the same result.
Contract-Neutral: Clarify existing History diagnostics and protect mirrored documentation; no runtime, API, identity, access, retention or response-shape change.
Change-source: pulse-maintainer
## What changed and why
Reduce configuration-fetch audit overhead without hiding access events
(#2320). The suppression tracker now remembers at most 4,096
organisation/agent pairs and uses an expiry index instead of repeated
full-map scans. Existing agents keep daily suppression; unremembered
agents at capacity are conservatively audited on every fetch.
Token/configuration changes and every failure remain audited. No audit
history is deleted.
The Apprise confidentiality test now observes the autonomous queue
dispatcher instead of starting a competing manual batch. Its lifecycle,
retry, configuration and confidentiality assertions remain intact.
Preserve the exact 6.4.6 release-line mapping for the already-published
packet. The fallback to main incorrectly rejected its immutable source
during convergence. This maps that version only, not future patches, and
changes no candidate product source.
## Included source and upstream reconciliation
Complete new publication range:
`e6540da45da4de4bcb954463373d8ea9f196eb56..35d8c7c13d3665b2ffe2fe4fd61e8952c46e21b1`.
- Reviewed Delivery correction:
`3578b0044740087e7589f19bca450245f610aced`.
- Reviewed Core changes: `e225bf2abcb4189dcedd457292248e72a945dc74` and
`6ab5383412f156f286c38623f9b86f2b0d3151f0`, integrated by
`94ce8496b7d79e9b77d43493adc432c954d5e8b7`.
- Additive upstream merge: `35d8c7c13d3665b2ffe2fe4fd61e8952c46e21b1`,
incorporating published Proxmox table repairs at
`e6540da45da4de4bcb954463373d8ea9f196eb56`.
Every reviewed commit identity and ancestry is preserved. No upstream
frontend source or dependency manifest changes in this proposal.
## Validation and limits
Recorded independent-review evidence: Core returned four complete
source-proof receipts covering connected
configuration-fetch/authorization tests, five race repeats,
notifications correctness/races and ten queue-fixture repeats,
vet/builds, 135 governance tests, 23 docs-sync tests and frontend
production build/budgets. Its fixed controls retained a maximum of 4,096
keys versus 12,288 in the predecessor; saturated in-memory lookup
measurements are not installed write-rate or CPU acceptance.
Delivery returned a complete offline source proof of 947 release-control
tests (8 skipped), with guest shutdown. The exact-version tests reject
unrelated versions. These are reviewed owner returns, not new
qualification executions by this publication gate.
On the combined committed source, provenance, whitespace, ancestry,
canonical completion, browser-verification scope, control-plane audit,
all 62 shipped-document mirrors and the 211-file public-document check
pass. Core runtime and Go manifests are byte-identical to the reviewed
Core candidate; release-control source/policy is byte-identical to the
reviewed Delivery candidate; frontend runtime and graph are
byte-identical to current upstream. Sufficient heavy/browser evidence
was therefore not replayed.
Core used Go 1.26.7 rather than declared 1.26.8, with an explicit
backend embed fixture and a separately proved frontend build. Required
exact-head CI remains necessary before landing. This proposal does not
establish installed outcomes, release readiness, successful convergence
or stable delivery, and does not replace or recut the published v6.4.6
candidate.
[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
Accept exact candidate 34f73590bb with retained source and browser proof. Resolve only the shared receipt pointer; runtime content remains exact to its non-merge proof commits.
Change-source: pulse-maintainer
The Workload Controllers table printed Job and CronJob timestamps
exactly as the agent reports them, so at 1440px the Detail cell read
"Last success: 2026-10-02T19:59:14.523639+01:00" in a 119px column
and was cut off before the time. A user could not tell when the job
last ran.
The cell now says "Last success 31m ago" or "Completed 2h ago" through
the shared platform relative-time formatter, with the absolute local
time on hover, matching how the Events table shows its observed time.
The Detail column takes 12% instead of 9% so every label the column
can produce fits whole, taken from the Kind, Current and Available
columns which hold short values. The Detail span truncates to the cell
with an ellipsis instead of being cut at the cell edge.
Contract-Neutral: presentation of existing Kubernetes controller time
fields only; owning unified-resources clause updated; no
frontend-primitives clause, primitive, or API change
Repeated device paths and UnRAID group names must not attach foreign SMART, temperatures or topology counts to a pool. Preserve canonical children, require host or scoped native ownership for inferred membership, and compare exact device identifiers. Cover live target changes and real desktop/phone presentations.
Change-source: pulse-maintainer
Avoid a needless subscription when Apps has no inventory, preserve second-attempt stream cleanup and bounded log-tail completion, and record only classified local socket-close causes without private wire data. This repairs the empty-inventory path in #2396; the exact rejection envelope and the cause of #2400 remain unconfirmed.
Change-source: pulse-maintainer
The mock generator stamped each job's last and next sync once at
startup, with the last sync up to an hour old on a 15-minute schedule.
Most jobs therefore started out overdue, and on a long-running demo
every job read as hours overdue in red while still reporting Healthy.
The demo's Replication tab looked broken to anyone evaluating Pulse.
Replication timing now follows the fixture clock. Each job keeps a
stable phase inside its schedule window, so a healthy job last synced
less than one interval ago and is next due in the future, and a
failing job's last good sync falls behind by its failure count.
The target node was picked by index and could equal the source, which
showed a job replicating pve1 to pve1. The source is no longer a
candidate, and an estate without a second node gets no jobs.
Preserve candidate 7c48430d5a and its exact runtime bytes alongside the reviewed main frontier. Resolve only the shared browser receipt file to the candidate receipt; original non-merge receipts remain the integration evidence.
Change-source: pulse-maintainer
Preserve the reviewed local tip and incorporate current upstream additively. The frontend/table changes are disjoint from the audit, Apprise fixture and exact-version release mapping corrections.
Change-source: pulse-maintainer
The Workload Controllers table printed Job and CronJob timestamps
exactly as the agent reports them, so at 1440px the Detail cell read
"Last success: 2026-10-02T19:59:14.523639+01:00" in a 119px column
and was cut off before the time. A user could not tell when the job
last ran.
The cell now says "Last success 31m ago" or "Completed 2h ago" through
the shared platform relative-time formatter, with the absolute local
time on hover, matching how the Events table shows its observed time.
The Detail column takes 12% instead of 9% so every label the column
can produce fits whole, taken from the Kind, Current and Available
columns which hold short values. The Detail span truncates to the cell
with an ellipsis instead of being cut at the cell edge.
Contract-Neutral: presentation of existing Kubernetes controller time fields only; owning unified-resources clause updated; no frontend-primitives clause, primitive, or API change
Preserve exact Core candidate commits after independent diff and retained-proof review. Bound suppression state without evicting recent agents or hiding audit events; retain the real-worker confidentiality assertions.
Change-source: pulse-maintainer
Real Chromium and WebKit first taps selected the latest reading instead of the tapped observation. Select and retain the actual sample across compatibility focus and mouse events, shared-chart focus transfer and matching refreshes. Reject pan, cancellation and multiple contacts without preventing native scrolling. Preserve mouse and keyboard inspection, source ownership, units and access.
Change-source: pulse-maintainer
The mock generator stamped each job's last and next sync once at
startup, with the last sync up to an hour old on a 15-minute schedule.
Most jobs therefore started out overdue, and on a long-running demo
every job read as hours overdue in red while still reporting Healthy.
The demo's Replication tab looked broken to anyone evaluating Pulse.
Replication timing now follows the fixture clock. Each job keeps a
stable phase inside its schedule window, so a healthy job last synced
less than one interval ago and is next due in the future, and a
failing job's last good sync falls behind by its failure count.
The target node was picked by index and could equal the source, which
showed a job replicating pve1 to pve1. The source is no longer a
candidate, and an estate without a second node gets no jobs.