Configured report schedules and recent attempts do not show successful
delivery or repeat use. Add durable aggregate reporting counters to the
existing opt-out-controlled heartbeat, covering resources, VM inventory,
backup health and Patrol digests.
Count successful generation, saved files, SMTP acceptance, definite
delivery failures and uncertain acceptance separately. Track distinct
successful saved-schedule periods and active days, including manual
downloads. Deduplicate retries and same-period reruns for repeat-period
counts. The adoption report distinguishes unmeasured senders and partial
observation from measured zero use. Generation across multiple days is
reported separately from successful repeat periods because it can
include retries.
No report contents, recipients, raw schedule or tenant identifiers,
scopes, errors or date ranges enter these new telemetry fields. Mock
activity is suppressed. Pricing, billing, customer entitlements and the
existing outbound opt-out remain unchanged. Update the schema-v20
disclosure and privacy document.
The shared telemetry observer has an exact-path adapter declaration.
Boundary regression fixtures reject sibling reporting implementation and
private licensing imports.
Validation: reporting persistence, API delivery/retry/restart and
successful-write tests, server snapshot tests, the full telemetry
package, 45 adoption-report tests, frontend type-check and build, and
governance audits passed. The three telemetry browser tests and a
separate pixel interaction check passed against the final source at
1280, 390 and 768 pixels, including saved opt-out states and the privacy
table. Source-bound browser receipt included.
Companion receiver: rcourtman/pulse-pro#472. Receiver deployment and a
client release are separate activation steps. This change alone does not
start production collection.
A scheduled retry across UTC days can generate another artifact without
completing a new successful reporting period. Keep multi-day generation as
an activity observation rather than inferred repeat use, while retaining
successful distinct-period counts as their own signal.
Exercise both cross-day generation without success and multiple successful
periods on one day so the adoption summary cannot conflate these metrics.
The usage observer records aggregate outcomes through the existing settings
store without implementing a paid renderer or entitlement decision. Declare
its exact shared-adapter path so the repository audit can distinguish it
from new private reporting implementation.
Exercise the real audit against sibling paths and private licensing imports
to retain those restrictions for the observer.
Record successful response writes, scheduled generation, file saves and SMTP
outcomes in a durable content-free tally. Distinct active days include manual
exports, while saved-schedule periods deduplicate retries and monthly reuse.
Publish bounded scalars through the existing opt-out-controlled heartbeat and
report missing coverage explicitly. Preserve pricing and customer entitlements.
Release candidate cut from main at 0d5b877425. It carries every change merged since stable v6.5.0.
Contract-Neutral: Release-only v6.6.0-rc.4 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
Retain backup execution outcomes and collection coverage in each
organization's recovery store so provider inventory changes and restarts
cannot erase report evidence. Keep missing and incomplete history distinct
from an observed period with no attempts.
Reuse existing report scopes, calendar schedules, branding and delivery
recovery for client PDF and CSV reports. Register the paid renderer through
the existing private runtime boundary. Pricing and grants are unchanged.
VM inventory was available only as an on-demand download. It can now be
scheduled weekly or monthly as CSV, emailed or saved on the Pulse
server. Each attempt exports all current VMs in the organization,
including newly discovered VMs, through the existing exporter.
Historical performance-report limits and windows remain separate.
The schedule uses existing delivery configuration checks, persisted
success/failure/uncertain states, bounded retries and deliberate
recovery controls. Pricing, feature gates, retention entitlements and
existing customer grants are unchanged.
Live checks also reproduced two unattended inventory defects: reporting
retained a stopped monitor after configuration reload, and canonical
snapshot reads relied on dashboard reads to publish polled VM inventory.
Reporting now receives reload replacements through an atomic manager
reference. Canonical snapshot reads reuse the existing publication
boundary, including its freshness fast path, provider/mode fences and
nonblocking warm reads.
Validation:
- 105 focused frontend tests; full type check, lint, production build
and bundle budgets.
- Race checks for reporting, inventory, reload, publication and
organization isolation; clean private runtime build.
- Current runtime browser checks on `/settings/support/reporting` and
`/alerts/notifications` at 1440×1000, 820×844 and 390×844, with actual
pixel review. The first inventory read exports 61 normally polled VMs;
real MIME CSV matches the download; a newly discovered 62nd VM enters
the next export.
- Local SMTP acceptance, 451 retry across a real restart, permanent
rejection, uncertain acknowledgement and deliberate recovery; disk
delivery, edit/reload, disable, details and delete.
- Staged status, registry, contract, completion and exact-content
browser guards pass. Regression proof is registered against the specific
runtime paths it covers.
Reuse the canonical inventory exporter and report delivery workflow so
operators can email or save a current organization-wide inventory weekly
or monthly. Keep delivery checks, bounded retries and recovery states
shared, with existing pricing and customer entitlements unchanged.
Refresh the shared inventory for unattended reads and rebind reporting
after monitor reload so schedules use the current infrastructure state.
The canonical completion guard checks each commit on its own; the
delivery commit relies on the following commit for its contracts and
proof files, so record that exact pair.
The canonical completion guard checks each commit on its own; the
classifier commit relies on the following commit for its ai-runtime
contract, so record that exact pair.
ensureAdminSession, the session check behind RequireAdmin and the
settings scope helpers, admitted anyone who could manage the
organization named in the request. That was meant for the org-bound
settings pages, but it applied to every admin route. On a multi-tenant
install an organization owner could therefore rotate the instance
administrator's API token and receive its secret, widen any token's
scopes, add or replace SSO providers, change system settings, relay
and log level, apply or roll back updates, restart the service, and
read instance logs and diagnostics.
ensureAdminSession now admits only instance administrators: the
configured admin or an instance RBAC admin grant, whichever org the
request names. Routes whose handlers act only on the request
organization opt in explicitly through RequireOrgAdmin,
ensureOrgAdminSession or ensureOrgSettingsWriteScope, which also admit
that organization's owners and admins. Those routes cover agents,
nodes, connections, availability, TrueNAS and VMware, agent profiles,
install commands, deploy status, configuration transfer and the
org-bound relay mobile token. Anything that does not opt in fails
closed.
Reporting stays instance administration, and the security status
snapshot stops offering it to org managers, because report generation
still reads the instance's default metrics store. Notifications,
LAN discovery and the action-capability fallback also stay instance
administration. The two credential routes no longer need their own
check, so ensureInstanceAdminSession is folded into this rule.
The canonical completion guard checks each commit on its own. The
admin split commit relies on the following commit for its dependent
agent-lifecycle and security-privacy contract updates and the route
inventory proof, so record that exact pair.
The route inventory treated RequireOrgAdmin routes as unauthenticated
"bare" routes, and the agent diagnostics contract and the API
reference anchors still named RequireAdmin for routes that now opt
into organization authority. The security-privacy contract still said
org managers receive reportingRead through ensureAdminSession; it now
names the org-bound routes' check and records that instance
administration refuses organization owners. The agent-lifecycle
contract notes that agent routes declare that authority explicitly.
The non-required **Unified Agent Native Verification** workflow has
failed on `main` (and most PRs) in both macOS jobs since 12 Oct (first
failing main run 38285211868 on c3524d373a; last green 38273335159 on
552e838749). The single cause is
`TestInstallSHUpstartTeardownRequiresObservedQuiescence`: 13 Python
subtests in `agent_upstart_teardown.py` fail with `ln: illegal option --
T`.
`scripts/install.sh` uses `ln -T -- <pending> <override>` and `ln -T --
<retired> <conf>` in the DSM Upstart teardown (added in the DSM
retirement series, a7271f5840 / b7b0eb0b01). That path only runs on
Synology DSM (Linux), so the installer is correct. The harness runs
those functions with the host `ln`, which on macOS runners is BSD `ln`
without `-T`.
The workflow already supplies GNU `stat/chmod/chown/mv/tar` (plus Bash
5) for Linux-only installer fixtures on macOS. This adds `ln` to that
set and extends `TestNativeInstallerFixtureToolsAreRealAndStepLocal` to
require it (missing/non-GNU scenarios, PATH binding, and real `ln -T`
no-replacement checks against file and directory targets).
Verification:
- Reproduced locally on macOS: the harness fails 13/15 with BSD `ln` and
passes 15/15 when `ln -T` is available.
- `TestNativeInstallerFixtureToolsAreRealAndStepLocal`: fails against
the old workflow (real tools, missing ln, non-GNU ln) and passes with
this change (Linux).
- The macOS jobs themselves are only exercised by this PR's CI run.
Commit a5baa7563b stopped Patrol reading cache-inclusive Proxmox memory
as guest pressure (issue #2762, a QEMU VM). It also put
`cluster-resources` in that cache-inclusive group for every guest type.
An LXC without a linked Pulse agent only ever gets `cluster-resources`,
so Patrol dropped memory pressure for all agentless containers:
- a container at 97% of its limit got no triage flag, forecast or
anomaly
- the seed said "guest pressure unknown; may include reclaimable cache"
- older memory alerts went to a billed model review on every run, and
any resolve verdict was thrown away
**That premise is wrong for LXC.** On both PVE 9 (`master`) and PVE 8
(`stable-bookworm` / `stable-8`), Proxmox subtracts page cache before
reporting a container's memory:
- `pve-common` `src/PVE/CGroup.pm` `get_memory_stat`: `$res->{mem} =
$mem - $stat->{file};` on cgroup v2 (`memory.current`), and `$res->{mem}
= $mem - $stat->{total_cache};` on v1 (`memory.usage_in_bytes`).
- `pve-container` `src/PVE/LXC.pm` `vmstatus` stores that value with
`$d->{mem} = int($mem->{mem});`. `PVE::LXC::CGroup` inherits
`get_memory_stat` from `PVE::CGroup` without overriding it.
- `pve-manager` `pvestatd` `update_lxc_status` writes `$d->{mem}` into
RRD field 8. `/cluster/resources` (`PVE/API2/Cluster.pm` →
`PVE::API2Tools::extract_vm_stats`) returns `$entry->{mem} = ($d->[8] ||
0) + 0;`.
A QEMU guest's listing `mem` is the host's view of guest RAM, which does
include the guest's cache, so it stays pressure-unknown.
**Change**
- `unifiedresources.QualifyGuestMemoryForKind` now takes the guest kind:
other platform, Proxmox VM or Proxmox container.
- A current, dated `cluster-resources` reading counts as known pressure
for a Proxmox container and stays cache-inclusive and unknown for QEMU.
- `status-mem`, `status-freemem` and `derived-total-minus-used` are
unchanged for every guest type.
- `GuestMemoryEvidenceForResource` takes the kind from the resource
type, so the Patrol inventory, the observer runtime and AI query
projections agree.
- `QualifyGuestMemory(bool)` keeps the conservative QEMU reading for
callers that don't know the guest type.
- The wrong comments in `calculateLXCMemory` and the LXC monitor tests
are corrected.
- Everything else in a5baa7563b still applies: retained, undated,
unknown and unavailable readings still cannot establish pressure or
recovery.
**Tests**
- `TestPatrolAgentlessLXCListingMemoryIsPressure` (new) builds an LXC
and a QEMU guest at 97%, both on `cluster-resources`, through legacy and
canonical read state. The LXC gets a memory triage flag and alert review
reads its value; the QEMU guest gets neither.
- `TestGuestMemoryClusterResourcesIsTypeAware` (new) covers both guest
kinds, retained readings, the other cache-inclusive sources and the
resource-type mapping.
- The `cluster-cache` fixture in `TestPatrolGuestMemoryEvidence` now
expects pressure for the container variant. It previously encoded the
wrong behaviour.
- Against origin/main's production files (`go test -overlay`), the new
and updated Patrol tests fail; with this change they pass.
Refs #2762
Supersedes #3011, which failed the documentation recipe test that pinned
the old `QualifyGuestMemory(` call in Patrol; this commit also updates
that pin to `QualifyGuestMemoryForKind(`.
`scripts/installtests/agent_upstart_teardown.py` created its fixture job
directory with a bare `mkdir`. Under a caller umask of 002 (the default
on Ubuntu hosts with user-private groups, pulse-dev included) that
directory came out 775. The installer's `trusted_lifecycle_regular_file`
check then correctly refused the fixture config, and running the harness
directly failed 23 subtests. The Go driver
`TestInstallSHUpstartTeardownRequiresObservedQuiescence` still passed,
because the installtests package already pins umask 022 for its children
(`umask_unix_test.go`). So the bug only shows up when the harness runs
on its own.
The fix gives the job directory an explicit 0755 mode. The sibling
TrueNAS and uninstall-state harnesses already create their trust-checked
directories with explicit modes. The production functions under test
still run with the caller's umask. They `chmod 644` what they write, so
nothing in production depends on umask.
Checked on pulse-dev (python3 3.12, go 1.26.5):
| run | umask 0002 | umask 0022 | umask 0077 |
| --- | --- | --- | --- |
| harness before | FAILED (failures=23, errors=1) | OK (15) | OK (15) |
| harness after | OK (15) | OK (15) | OK (15) |
| Go driver after | ok | ok | — |
The other Python harnesses in `scripts/installtests/` (legacy, OpenRC,
systemd, TrueNAS, uninstall-state) already pass under umask 0002, so
they are unchanged.
Test-only change.
An existing collector can retain its legacy TLS bypass after the host
trusts the Pulse certificate. Updating it with action-runner enrollment
then incorrectly rejects normal system-root trust and enters recovery
before creating the runner service.
Keep collector/download settings separate from verified runner TLS.
HTTPS runner configuration continues to use system roots, a configured
CA or an exact certificate pin and never inherits the collector bypass.
Activation, cancellation, revocation and service recovery retain their
existing boundaries.
Validation: the new executable regression fails against the previous
installer and passes with the fix. It covers recovered insecure state,
private configuration persistence and the real runner credential
lifecycle binary over TLS. System roots, custom CA and exact pin
succeed. Untrusted certificates and incorrect pins receive no
credential-bearing request. The installer Action Runner suite, runner
binary tests and hostagent Action Runner suite pass.
Agent Doctor enrollment also passes in the current built app at 1440,
820 and 390 pixels wide, covering details disclosure, issue
failure/pending/success, private command copying, one-time reveal,
clearing and reload. Browser endpoints use synthetic fixtures. No native
installed-host or published-release acceptance is claimed.
The additional native workflow passes on Linux x64 and ARM64. Its macOS
Intel and ARM64 jobs fail the unchanged Upstart retirement contract
because the host `ln` rejects `-T`. The same failure occurs in [the
earlier main
run](https://github.com/rcourtman/Pulse/actions/runs/38899584939). This
change does not touch that fixture or the exercised retirement
functions.
## Summary
Unattended updates on small Proxmox LXCs were being OOM-killed. The
server installer staged the release archive (~280 MB for v6.5.0) and its
extracted tree (~750 MB) under `/tmp`. On Debian 13 and many LXC
templates `/tmp` is tmpfs, and tmpfs pages count against the container's
memory cgroup. The reporter's host log shows the `/lxc/118` cgroup at
its 1 GiB limit with ~820 MB of shmem, the kernel OOM-killing the
container's `systemd` (PID 1), and Proxmox then stopping the container.
`install.sh` now downloads and extracts into owner-only hidden
directories under the install directory:
- The install directory is always writable from the hardened
`pulse-update.service` sandbox.
- The staged binary already lives there.
- The disk headroom preflight now measures the filesystem the update
actually stages on.
The Go updater already prefers its data directory over `/tmp` for the
same reason.
Two follow-ups that come with staging on disk:
- **Stale staging sweep.** An interrupted run's staging (~1 GB) now
survives reboots. Staging setup removes stale `.pulse-extract-XXXXXX`
and `.pulse-<release>-linux-<arch>-XXXXXX` directories under the install
directory. It only removes real directories with those exact names,
never symlinks or files, and only when their ctime is more than 6 hours
old. `install.sh` has no update lock, so age is what proves no live run
still owns them.
- **Ownership.** Every recursive `chown -R pulse:pulse "$INSTALL_DIR"`
(archive install, source build, `setup_directories`) now goes through
`chown_install_dir_for_pulse`, which skips staging. The payload that
root still reads is never handed to the service account.
The `Failed to print table: Broken pipe` line in the report came from
`systemctl list-unit-files | grep -q` in v6.5.0's service discovery.
`main` already reads that inventory in full, and it was not the cause of
the failure.
## Tests
- New Go tests `TestRootInstallScriptSweepsOnlyStaleServerStaging`
(fresh staging kept, stale staging removed; look-alike names, files,
symlinks and their targets kept) and
`TestRootInstallScriptPulseOwnershipExcludesServerStaging` (staging
skipped, no raw recursive install-dir chown left).
- New `test_archive_extraction_stages_on_the_install_filesystem` (server
installer stop harness) and
`test_download_stages_archive_on_the_install_filesystem` (temporary
inputs harness) fail against the current installer and pass with this
change.
- `scripts/tests/test_server_installer_stop.py`,
`test_server_installer_temporary_inputs.py`,
`test_server_service_discovery.py`, and the `TestRootInstall*` tests in
`scripts/installtests` pass (run on Linux).
Refs #2785
agent_upstart_teardown.py created its fixture job directory with a bare
mkdir, so under a caller umask of 002 (the default on pulse-dev and other
Ubuntu user-private-group hosts) the directory came out 775. The
installer's trusted_lifecycle_regular_file check correctly refuses a
config whose parent is group-writable, and 23 subtests failed when the
harness ran directly. The Go driver only passed because the
installtests package pins umask 022 for its children.
Give the job directory an explicit 0755 mode, matching the sibling
TrueNAS and uninstall-state harnesses, which already create their
trust-checked directories with explicit modes. The production code
under test still runs with the caller's umask; it sets 644 on the files
it writes, so the harness now passes under 002, 022 and 077.
Patrol treated every guest's cluster-resources memory as cache-inclusive,
so an LXC without a linked Pulse agent, which only ever gets that source,
lost memory pressure entirely: no triage flag, forecast or anomaly at 97%
of its limit, a "may include reclaimable cache" caveat in the seed, and
older memory alerts sent to model review every run with any resolve
verdict discarded.
The premise holds for QEMU but not for LXC. Proxmox's container mem is
cgroup usage with page cache already subtracted: PVE::CGroup's
get_memory_stat returns memory.current minus memory.stat file (cgroup v2)
or usage_in_bytes minus total_cache (v1), PVE::LXC::vmstatus reports it
as mem, and pvestatd feeds that value to the RRD that /cluster/resources
reads. A QEMU guest's listing mem is the host's view of guest RAM and
does count the guest's cache.
Qualification now takes the guest kind. A current, dated cluster-resources
reading establishes pressure for a Proxmox container and stays unknown for
QEMU; status-mem, status-freemem and derived-total-minus-used stay
cache-inclusive for both, and retained readings still cannot prove
pressure. The typed read state derives the kind from the resource type,
so Patrol, the observer and AI queries agree. The calculateLXCMemory
comment that called the listing value cache-inclusive is corrected.
Contract-Neutral: monitoring change is a corrected comment in calculateLXCMemory and its tests; LXC memory collection and sources are unchanged
The DSM Upstart removal path links its manual override and restores a
retired job config with GNU `ln -T --`, a hard link that never replaces
or descends into an existing target. Its Python harness executes those
production functions with the host's ln. On the macOS runners that is
BSD ln, which rejects -T ("illegal option -- T"), so 13 subtests of
TestInstallSHUpstartTeardownRequiresObservedQuiescence have failed on
both macOS jobs since the DSM retirement series landed on 12 Oct.
The production code is right: that path only runs on Synology DSM
(Linux), never on a macOS agent host. The gap is the macOS fixture
toolset, which already supplies GNU stat/chmod/chown/mv/tar for these
Linux-only installer contracts but not ln. Add ln to that step and make
the fixture-tool contract test bind it and exercise real `ln -T`
no-replacement semantics (file and directory targets).
Contract-Neutral: CI fixture tooling only; macOS runners gain the GNU ln the existing DSM Upstart harness already assumes, no installer or lifecycle contract change
The server installer downloaded the release archive and extracted it
under /tmp. Debian 13 and many LXC templates mount /tmp as tmpfs, whose
pages are charged to the container's memory cgroup. A v6.5.0 archive is
~280 MB and extracts to ~750 MB, so an unattended update on a 1 GiB LXC
filled the cgroup with ~820 MB of shmem; the kernel OOM-killed the
container's systemd and Proxmox stopped the container mid-update.
Download and extract into owner-only hidden directories under the
install directory instead. It is always writable from the hardened
pulse-update.service sandbox, it is where the staged binary already
lives, and the disk headroom preflight now measures the filesystem the
update actually stages on. The Go updater already prefers its data
directory over /tmp for the same reason.
Unlike /tmp, the install filesystem keeps an interrupted run's staging
across reboots. Staging setup now removes stale staging directories that
match only the exact owner-only names, never symlinks or files, and only
once their ctime is over six hours old: install.sh has no update lock,
so age is what proves no live run still owns them. Every recursive
pulse chown of the install directory now goes through one helper that
skips staging, so the payload root still reads is never handed to the
service account and the ~750 MB tree is not rewritten for nothing.
The "Failed to print table: Broken pipe" line in the report was
systemctl list-unit-files | grep -q in v6.5.0's service discovery; main
already reads that inventory in full, and it was not the failure.
Contract-Neutral: Defect fix inside the existing private server-installer inputs and update-space boundaries; staging moves from tmpfs-prone /tmp to the install filesystem with no public contract change
Legacy updates recover the collector's insecure setting even after the host
trusts the server certificate. Do not reject verified runner system trust
because of that unrelated setting. Keep HTTPS runner verification enabled
and preserve custom CA and exact fingerprint trust.
Exercise recovered configuration and the real credential lifecycle client
against TLS, including untrusted certificates and mismatched pins that must
receive no credential-bearing request.
The hourly "Reconcile release convergence debt" job has failed every run
since 2026-10-13T03:57Z with a bare "GitHub object was not found". The
v6.5.0 activation marker names convergence run 37395942598 as its owner,
and that run (with the rest of the 30 Sep - 8 Oct release run history) was
deleted from Actions between 03:11 and 03:57Z on 13 Oct. The immutable
marker outlives the run, so the owner lookup now 404s.
Because the stable head's run sorts first and any error aborted the loop,
the failed v6.6.0-rc.2 convergence (38927798751) has never been reconciled.
Reconcile each selected channel independently and still fail the job if any
channel failed, so one channel's broken evidence cannot starve the other.
Name the deleted owner run explicitly instead of a generic 404, and do not
retry it: release-convergence.yml waits for that owner to read "completed",
so a fresh dispatch could only time out.
Send draft verification and fingerprint settings to the saved-credential
probe so Test connection reflects the edit form before saving. Preserve
saved defaults for callers without overrides and keep probe state temporary.
Keep shared footer actions visible on narrow screens so the edit and test
flow remains usable through pending, error and confirmation states.
Release candidate cut from main at 5a66a7068d. It carries every change merged since stable v6.5.0.
Contract-Neutral: Release-only v6.6.0-rc.3 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
Measure delivered extended history before changing free and paid features.
Count only successful responses containing an older metric point and retain
only bounded daily totals. Expose the rolling sum through schema v19 and
Preview with matching privacy disclosure and explicit report coverage.
Release candidate cut from main at 9f84b9b05e. It carries every change merged since stable v6.5.0.
Contract-Neutral: Release-only v6.6.0-rc.2 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
Guest investigations could miss collected backup failures, and canonical
storage IDs could not retrieve metric history. Investigations now read
backup attempts through the same org-scoped recovery source as the HTTP
API and storage history through the monitor-owned source used by charts.
Responses preserve error text, timestamps, requested windows, read
failures and synthetic provenance. Dense history sampling also includes
the newest remainder observations, which the previous capped fixed-size
buckets discarded.
Regression coverage checks guest tool projection, persisted backup
attempts, resource and tenant isolation, demo-mode transitions, retained
storage history after restart, measured zero, newest observations in
dense series and API/tool coherence. Focused backend tests and both
private runtime builds passed on the validation worker. Frontend
type-check and build passed.
The separate backup inventory overview still uses legacy identifiers.
That remaining canonicalization work is recorded in the existing L6
architecture follow-up.
Browser verification exercised saved investigation summaries and
expanded tool evidence on `/patrol` at 1440×900 and 390×844, including
keyboard open/close, reopen and reload. This was a read-only view of
synthetic investigation records, not action or release qualification.
Guest investigations could miss collected backup errors and could not read
storage history through canonical resource IDs. Share recovery point reads
with the HTTP API and monitoring history with charts, preserving tenant and
resource scope, timestamps, provenance, window bounds, and read failures.
Cover manifest projection, persisted recovery tasks, demo source parity,
retained storage history after restart, and API/tool evidence coherence.
## What changes for users
**Alerts › History** picked its table or its phone cards by screen
width: the table from 768px up. With the Alerts sidebar open, though, a
1024px to roughly 1240px window leaves the table only a 600–800px card.
At that width the table:
- stacks its three row actions on top of each other;
- breaks resource names mid-word ("inventory-/api");
- cuts messages off after two lines.
The cards are now chosen by the card's own width:
| Window | History card | Layout |
| --- | --- | --- |
| 1280px, sidebar open | 837px | table (unchanged) |
| 1024px, sidebar open | 602px | cards, full names and messages |
| 900px (sidebar becomes tabs) | 774px | cards |
| 375px phone | 341px | cards (unchanged) |
An open timeline or resource view follows you across the switch: it
moves into the cards' drawer when the window narrows, and back into the
table row when it widens.
## How
- **`AlertHistoryTableSection` measures its card.** It uses the
canonical `useObservedElementWidth` and shows the cards below 800px
(`ALERT_HISTORY_TABLE_MIN_WIDTH`), the width from which the row actions
share one line. Until the card is measured, the phone breakpoint decides
as before. The decision is a memo.
- **`AlertHistoryMobileList` takes an `active` signal** in place of its
own `matchMedia('(min-width: 768px)')`. It memoises that signal and
moves the shared expansion state into its drawer only when the layout
actually changes. Without the memo, every resized pixel re-ran that move
and could reopen a drawer the operator had closed.
- **Both layouts stay mounted, as before.** The hidden one gets
`hidden`.
- **`registry.json`.** `AlertHistoryMobileList.test.tsx` and
`AlertHistoryTableSection.test.tsx` join the alerts frontend-surface
proof files. They are the tests that prove this surface.
## Verification
- **Tests.** New tests cover:
- the width decision (`AlertHistoryTableSection.test.tsx`);
- layout handover and the memo through a width-derived signal
(`AlertHistoryMobileList.test.tsx`). With the list's memo removed, that
test fails.
All four new or changed tests fail against main.
- **Suites.** The alerts, Alerts components and pages suites pass (2,497
tests). tsc, prettier, the copy, canonical-platform and form-label
audits, and the bundle check are clean.
- **Live (mock estate).** The table-or-cards results match the table
above. A card timeline opens the drawer at 1024px. An inline timeline
survives 1280 → 1024 → 1280. The 375px phone has no sideways scroll.
- **E2E.** The Playwright desktop project (Desktop Chrome, 1280px) keeps
the table.
- The browser receipt is included.
Alerts > History chose between its table and its phone cards by the
viewport: the table from 768px. With the Alerts rail open, a 1024px to
roughly 1240px window gives the table a 600px to 800px card, where its
own CSS stacks the three row actions and resource names and messages
break mid-word ("inventory-/api") or end after two lines.
The table section now measures its card with useObservedElementWidth and
shows the cards whenever the card is narrower than 800px, the width from
which the row actions share one line; before the card is measured, the
phone breakpoint decides as before. The cards take that decision as an
`active` signal instead of their own 768px media query, so an open
timeline or resource view still moves into the cards' drawer when they
take over and back to the inline expansion when the table returns. The
decision is a memo so a resize within one layout does not re-run that
move and reopen a drawer the operator closed.
Show optional screenshots directly after highlights and move routine upgrade
instructions into their own disclosure. Keep known issues and breaking changes
expanded before the highlights, and tell release authors to place critical
risks in the visible warning section.
Put published highlights and optional screenshot comparisons in the
post-update dialog, with grouped details available on demand.
Reuse inspected images from merged pull requests or capture fresh release
views, then publish versioned PNG sidecars. Serve screenshots through the
authenticated release endpoint to preserve the existing image policy.
Release candidate cut from main at 84a9694018. It carries every change merged since stable v6.5.0.
Contract-Neutral: Release-only v6.6.0-rc.1 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
The frequency chart drew every period in one blue under a legend of
coloured severity dots that described nothing in the bars, left info
alerts out of that legend, and used a floored log scale on which a
25-alert burst looked barely three times one alert.
Each period now stacks critical, warning and info in the legend's own
colours, counted with the alerts contract's closed severity vocabulary,
and bars scale by the square root of their share of the busiest period so
a burst stands clear while quiet periods keep a readable mix. The legend
counts all three severities, and the tooltip and accessible name carry
each period's breakdown. Light-theme warnings use amber-700 because
yellow on the chart surface was under 2:1, and a selected period is
ringed instead of dimming the rest below 3:1.
The chart's two test files are registered as alerts frontend proofs.
Sixteen worker-browser proofs still opened the removed History tabs,
clicked the removed Refresh button or depended on UI that has since
moved. Each now passes on pulse-dev:
- Charts are read on Overview (guest, resource, PBS) or inline in disk
and pool details; table-row-touch toggles Manage/Overview as its
nested input instead of the old tab.
- Reads are driven through the 30-second poll with the page clock.
- Locators select series paths and hover markers, since bands and
observation dots also draw paths.
- Rows lifted into Worth checking or kept under the closed Details
disclosure are read from the whole Overview, with Details opened.
- With stored history shown, the legend reads the latest stored time,
so series without a point say "no reading" rather than a live value.
- Stubbed history replies echo the requested series and carry min/max,
as the client has validated since 866da85c26.
- Fixtures that mount the resource drawer supply activeAlerts.
- The guest-stopped-disk write check allows the content-free
workload-history usage milestone that opening details records.
- On emulated phones, headless Chromium reports a fine pointer, so the
proof touches the plot before tapping the inspection slider.
The drawer tab-retention proof (#1723) and the PBS host history
correlation proof share browser-tests/pbs-host-history-correlation.tsx.
That fixture mounted the Backups table without the app runtime context
the drawer Overview reads alerts from, so both proofs threw before the
drawer rendered and had silently stopped proving anything.
The fixture now supplies an explicit offline WebSocket and dark-mode
context and a router, as the other drawer fixtures do. The correlation
proof follows the drawer's current shape: history charts sit on
Overview, so it checks that Overview stays selected with stable chart
paths across refreshes. Since the Identity card shows each identifier
once, the Metrics Target row may be absent; when present it must name
the host series, and the intercepted history request still must.
Both proofs pass at 1280 and 390px on every topology. Negative controls
fail as they should: resetting the tab when the metrics target drops
breaks the retention proof, and charting the PBS service key breaks the
correlation proof.
Beta cut from main at 1844f6c0ed. It carries every change merged since stable v6.5.0.
Contract-Neutral: Release-only v6.6.0-beta.3 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
The beta release train writes cumulative changelogs from the previous
stable, but the install packet check required the previous numbered
beta. Accept both supported comparison modes and continue rejecting
unrelated, missing or ambiguous bases.
Validation: targeted race tests and the failed beta.3 snapshot
regression, plus governance audits and native commit and push checks.
The release train explicitly compares the full beta range with the previous stable. Accept that supported base as well as the default previous numbered prerelease, while rejecting missing, ambiguous and unrelated comparison bases.
Contract-Neutral: Align release packet checks with the existing metadata contract. No public install or runtime contract delta.
PR 2913 moved the Proxmox guest and node drawers' charts onto Overview
and removed their History tab. The Docker host drawer and the shared
resource-detail drawer (agent machines, containers, PBS, TrueNAS,
Kubernetes and other platform rows) still kept the charts a tab away,
so the same kind of drawer behaved two ways depending on the page.
Both now render the history charts on Overview under the attention
list, opening on 24 hours with the range picker beside them, and drop
the History tab. Charts stop polling while another tab shows, and an
active alert on a charted metric draws its trigger the same way as in
the Proxmox drawers. A resource whose metrics target drops for one
snapshot simply loses its charts until the target returns; the
selected tab is kept.
Storage page disk and pool details keep their own History tab; they
are separate components and their Overview has no charts to repeat.
E2E specs 27 and 63 no longer click History; the drawer read starts
when it opens. The manual tab-retention proof now selects Manage. Its
fixture page fails before the drawer renders (no WebSocket provider)
on main as well, so it was not run.
The Workloads table put a second line under memory and disk cells on
every stopped guest ("N/A / Unavailable", "- / Unavailable"), on every
agentless VM's disk, and "May include cache" under every agentless VM's
memory bar, even at 20%. None of them said why or what to do, and they
broke the row rhythm.
Memory that cannot be read now renders the same dash as an unreadable
disk, with its reason in the cell title, the sparkline's hover title
and assistive text. Disk shows a visible label only for a retained
reading (Last known) or a read the user can fix (No permission, Agent
error). The cache caption appears only when the guest's own memory is
at its warning or critical threshold, the figure that colours the bar
in host-share mode too, which is the case #2762 and #2639 were about;
below that the caveat stays in the title and assistive text.
An unavailable memory explanation names what is known instead of
"Unknown source": a powered-off guest reads "Unavailable while the
guest is powered off." MetricMiniSparkline gains an optional tooltip
so the chart covering the cell can carry that reason without changing
its accessible name.
Governance now reports explicit completion, residuals, evidence and
ownership availability. It no longer computes lane grades, completion
percentages or weighted candidate priority. Release qualification,
security assertions and lease boundaries stay enforced.
The frontend contract also drops obsolete Discovery surface catalogs and
preserves the live Assistant handoff and execution contracts.
Validation: 93 focused Python tests, repository-control Go tests, and
control-plane, status, registry and contract audits. Full delivery hooks
passed, including 164 helper tests and frontend audits after integration
with current main.
The first beta's page listed no changes, and the full changelog file is
neither attached to nor linked from any release page, so a reader could
not look up a change the summary did not name. Add a pointer, advisory
and additive.
The renderer appends a "## Source comparison" section after Roll back: a
link to GitHub's comparison of the release tag with the stable rollback
target the workflow already passes (--rollback-target and --candidate-tag).
It is a pointer, not a list. GitHub shows at most 250 commits in a
comparison and this beta had 328 changes, so the section says larger
comparisons may omit commits and is not called a full list.
It never fails a release on its own. The helper renders nothing, and
raises nothing, for a missing or malformed tag, a base that is not a
lower stable release, a trailing newline, non-ASCII digits, or numbers
too large to convert. Bodies without the section, including every
earlier release and any frozen in-flight one, keep validating, because
the validator already permits a trailing section and is unchanged. The
post-update changelog dialog reads only recognised category headings, so
a separate heading after Roll back is ignored by it.
Every consumer of the rendered body was audited (the renderer's
validators, create-release, validate-release-assets,
recover-release-activation, the updater backend, the what's-new model and
the Go and TypeScript tests): no regression. Review of the first draft
found a crash on a 5000-digit tag, a newline that would land inside the
URL, and overclaiming contract text; all fixed and pinned by tests that
fail against the first draft. A tags/ prefix would avoid a branch that
shares a tag's name, but GitHub's web comparison URL returned 404 for it
on existing tags, so bare refs are used and the limit is recorded.
Tests run the real renderer on the v6.5.0 notes: placement after Roll
back, exact spacing, numeric ordering, ten edit and annotation cycles
with CRLF and the extra jq newline, expected-body rejection of a changed
or deleted section, and absence still validating. They fail against the
original renderer. Making the section required, and a generated
row-level inventory that proves completeness, need a declarative contract
record in the preparation snapshot and are deliberately deferred; the
contract explains why.
Contract-Neutral: Adds an advisory link to rendered release pages; no published artifact, tag, signing, install or customer contract changes.
Preserve current Patrol setup and provider connection behavior alongside
the explicit governance completion and evidence contract. Inherited
application states retain their qualified main content.