The earlier drawer changes left a few surfaces that still looked or
behaved differently, plus code nothing used any more:
- Proxmox Mail Gateway had two drawers for one gateway; the row now
opens the shared resource drawer with mail flow as its Mail tab. The
Ceph cluster drawer gains the canonical header, Overview (alerts,
Worth checking, capacity history, cards, closed Details) and Manage,
and its dot follows the row instead of reading grey on missing health.
- The Kubernetes cluster drawer's Deployments tab repeated the Workloads
table with dead-end rows; it is gone, and Namespaces opens the
Workloads tab scoped to the cluster and namespace with one action.
The Docker Swarm drawer tab could not be reached from any page; gone.
- On phones, windowed tables (Kubernetes deployments, pods) and the
Storage tab's grouped pools stopped at 70-90% of the width: a
colspan=100 spacer or colspan=99 group row created phantom columns
that took the width hidden columns left. Those rows now span exactly
the visible header cells.
- The node and Docker host drawers gain the guest drawer's Open action;
the Docker host withholds readings while offline or stale, lists
operator state first in Manage and shows its action history. A
guest's physical disks open the same disk Overview as the Storage tab,
with the guest agent's disk temperature alerts.
- One "No active alerts" line, one timestamp row and wrapping alert
subjects replace per-drawer copies; PBS and PMG alerts match on their
instance id so drawers find them without a metrics target.
- Removes the old disk detail and its presentation code, the Swarm and
Kubernetes Deployments drawers and their presentation helpers, and
drawer state nothing read.
Alerts, events, tasks, services and backup rows open a "record" drawer:
something that happened to an object, not an object to manage. Many of
them were hand-built grids that looked like a different product: no
heading, no status, no way to close from inside, raw timestamps, labels
cut to three, and no link to the object they were about.
- The five Docker Swarm tables, Docker networks and storage usage,
Proxmox replication and coverage now open the shared record panel:
the subject's name and the row's own status dot, a one-line summary,
wrapping sections with human dates, and an "Open <name>" link where
the app can focus the owning object (and none where it would only
land on a whole tab).
- Recoverable restore points had no drawer, and on phones lost their
location entirely; they now open one.
- Kubernetes events, ConfigMaps, Secrets and RBAC objects opened the
full object drawer, which never showed an event's message and offered
maintenance windows on it. They open record drawers instead; a
Secret's key names and values are never shown.
- Coverage allowed several rows open at once; it now opens one, like
every other table.
- Agent Doctor targets open one at a time with a proper header, shared
notices and a closed Details for connection evidence.
- Availability fleet tiles behave as disclosures and open the same
table-row drawer as the table view.
The Proxmox guest and node drawers lead with what is wrong, chart the
last day and keep the long tail in one closed Details. Other drawers
did not:
- Storage pool and physical-disk details on the Proxmox Storage tab had
no heading, status dot or collapse control, no alerts line or Worth
checking, a pool chart defaulting to 7 days with its own picker, a
disk "History" and "Live I/O" section, and every config, topology and
SMART table open by default. Both are now object drawers in the shared
detail row, with Overview (alerts, Worth checking, 24-hour charts,
facts, ZFS/SMART cards, closed Details) and Manage (operator state).
A disk alert that already states the verdict is not repeated beneath
it, and worn flash is stated once by the verdict.
- TrueNAS ZFS errors and disk risk, vSphere datastores that are not
accessible, Kubernetes containers that are not ready and cordoned
nodes sat two or three closed disclosures deep and never reached
Worth checking. Platform sections now join the same split as every
other drawer: warnings lift, cards show, the rest waits in Details.
- Kubernetes Deployments, autoscalers, Services, Ingresses and volumes
had no section at all, so columns hidden on phones were unreachable.
Docker image and volume drawers showed nothing about the image or
volume.
No field is dropped; it moves to where the guest drawer keeps it.
The Proxmox guest and node drawers were reworked into a clear shape, but
the drawers on other pages kept older pieces that disagree with them:
- The generic resource drawer coloured its status dot from the raw
status while the row above it used each platform's own verdict, so a
succeeded pod read red, a stale machine read Online and a running but
failing container read green in the drawer while the row said
otherwise. Tables now pass the indicator behind their row dot, and the
drawer repeats it.
- Record drawers (alerts, activity, services, protection jobs) were
headed by a generic label such as "Alert detail", had no status dot,
named their collapse button after an internal id, and cut alert
sentences and identifiers with an ellipsis that phones cannot hover.
They are now headed by their subject with its status and a one-line
summary, take an actions slot for owner links, and wrap their values.
- Five tables (Ceph, Mail Gateway, Replication, Coverage, Docker
Networks) replaced the detail row's content class to change padding
and silently dropped its phone width cap. The shell is no longer
overridable, and a guard keeps it that way.
- DrawerSubjectHeading is now the one subject heading: names wrap rather
than truncate, and the generic drawer uses it with its source badges
beside the name, an Open action for a saved web URL, and no duplicate
health chip (the issue already leads Worth checking).
Verified on a full mock estate at 1440 and 390 against origin/main: every
drawer dot now matches its row and no route logs a console error.
Stacked on #3109 (`fix/mock-pve-guest-sighting-cadence`); once that
merges, this PR's diff reduces to its own commit.
## Problem
In mock mode, the update loop picks PVE inventory by node name, one
cohort at a time. Shared storage belongs to no single node: the PBS
stores use the owner `"cluster"`, and `shared-storage`, `ceph-rbd` and
`cephfs-data` use `"shared"`. The mock Ceph cluster has no node either.
None of them ever matched a cohort, so the loop never re-sighted them.
They kept their startup stamp and turned into stale Proxmox sources
within minutes, at every `PULSE_MOCK_UPDATE_INTERVAL`, the default 2s
included. The Ceph cluster was worse. `generateCephClusters` set its
`LastUpdated` from the wall clock and nothing touched it again, not even
the full `UpdateMetrics` refresh. Both readings show on the public
demo's Storage and Ceph views.
## Fix
Cluster-scoped inventory is sighted on the first cohort of each PVE
rotation. That is the cadence `mock.ProxmoxRefreshInterval` reports
(#3109), and the mock Proxmox stale threshold derives from it. A row is
sighted only while one of its reporting nodes is online; when a row
lists no reporting nodes, any online node of its instance counts. The
same selection also resamples shared-storage usage, which the cohort
loop skipped as well. The full refresh path keeps sighting everything.
**Mock-only.** The real storage poll stamps cluster storage with the
poll's start time on every poll (`monitor_polling_storage.go`,
`LastSeen: startTime.UTC()` for the `Node: "cluster"` rows).
`pollCephCluster` runs after every storage refresh and sets
`LastUpdated: time.Now()` (`ceph.go`).
## Proof
**Regression test:**
`TestFixtureGraphSlowTicksKeepProxmoxClusterSightingsFresh` drives the
loop's own cohort sequence through two rotations at 2s, 7s, 15s and 60s
ticks. It records each shared store's and the Ceph cluster's worst
sighting age just before a tick. It fails on #3109's head: at 15s ticks
every shared store reaches 5m0s and Ceph ~1h43m, against a 30s bound. It
passes with this change. The rest of the cohort and cadence tests in
`internal/mock` still pass, and the full `internal/mock` suite passes.
**Live before/after on pulse-dev.** Two builds ran side by side: #3109's
head (before) and this branch (after). Both used fresh data dirs and the
demo env (`PULSE_MOCK_MODE=true DEMO_MODE=true PULSE_MOCK_NODES=8 ...
PULSE_MOCK_UPDATE_INTERVAL=15s PULSE_MOCK_SEED_METRICS_STORE=true
PULSE_TELEMETRY=false`). Storage and Ceph resources from
`/api/resources` were tallied once a minute for 10 minutes:
| t | before: storage/ceph with a stale Proxmox source | before: oldest
sighting | after: stale | after: oldest sighting |
|---|---|---|---|---|
| +0m | 3 (Ceph, ceph-rbd, cephfs-data) | 167s | 0 | 20s |
| +1m | 8 | 227s | 0 | 20s |
| +2m | 8 | 287s | 0 | 20s |
| +5m | 8 | 467s | 0 | 5s |
| +10m | 8 | 767s | 0 | 20s |
On the before build, the 8 stale sources were the Ceph cluster, two
`ceph-rbd` and two `cephfs-data` rows, `shared-backup-fabric`,
`west-backup-primary` and `west-backup-offsite`. From t+1m onwards, the
before build showed `storage/warning/proxmox:stale=7` and
`ceph/warning/proxmox:stale=1`. The after build stayed at
`storage/online/proxmox:online=22` and `ceph/online/proxmox:online=1`
all through; its one Proxmox storage in `warning` has an online source.
## Why
A mock stack with the public demo's settings
(`PULSE_MOCK_UPDATE_INTERVAL=15s`, 8 nodes, 6 VMs + 4 LXCs per node)
drifts healthy running guests into Attention within minutes: `status:
"warning"`, `health.reasons: telemetry_stale`,
`sourceStatus.proxmox.status: "stale"`.
Root cause: the mock update loop sights PVE nodes and their guests
through ten fixed cohorts, sized for the default two-second tick (every
node within 20s). The count never followed the tick interval, so at 15s
each node's guests were sighted only every 150s, while the registry
judges Proxmox sightings stale after 60s.
## What
- `internal/mock`: the PVE estate rotates through the largest divisor of
the ten cohorts that still sights every node within 30s
(`proxmoxCohortCount`). At the default 2s tick nothing changes (10
cohorts). At 15s it uses 2 cohorts, and from 16s up every node is
sighted on every tick. A divisor keeps the loop's cohort index mapping
each node to exactly one tick per rotation. Per-second churn never
exceeds the 2s default.
- `internal/monitoring`: when a single tick is slower than that budget,
the mock Proxmox stale threshold is derived from the actual sighting
cadence (`mock.ProxmoxRefreshInterval`), never below the configured PVE
derivation. This mirrors how 1d9e66b24c derived the supplemental
provider thresholds.
- Contract notes in `monitoring.md` and
`performance-and-scalability.md`.
**Mock-only.** Real PVE polling builds every guest of an instance with
that poll's `sampleTime := time.Now()`, and its threshold already
derives from the poll interval or the adaptive scheduler's maximum. No
real poller goes through the cohort rotation.
## Proof
- `TestFixtureGraphSlowTicksKeepProxmoxGuestSightingsFresh` drives the
loop's own cohort sequence through two rotations at 2s/7s/15s/60s. It
fails on `main` (15s: "last sighted 45s ago, want within 30s"; 60s:
"2m0s ago") and passes with the fix.
- `TestProxmoxRefreshCadenceTracksSightingBudget` and
`TestResourceStaleThresholdsDeriveMockProxmoxCadence` pin the cadence
table and the threshold derivation.
- `internal/mock` and `internal/unifiedresources` pass in full.
`internal/monitoring` passes in full except one expected assertion
change: `TestMockUnifiedViewAppliesOperatorManualLinks` holds a 5-minute
mock tick, so its Proxmox threshold now derives to 10m instead of the
configured 4m. That test was updated and passes. `internal/api`
mock/stale/demo tests pass.
Live before/after on pulse-dev: `main` 08217d112e vs the same plus this
patch, both run with the demo's exact mock env and a fresh data dir. The
tally covers PVE guests across all `/api/resources` pages:
| t | main: warning / telemetry_stale (of 77) | fix: warning /
telemetry_stale |
|---|---|---|
| +60s | 25 / 28 | 0 / 0 |
| +180s | 53 / 58 | 0 / 0 |
| +300s | 64 / 70 | 0 / 0 |
| +480s | 49 / 53 | 0 / 0 |
| +600s | 64 / 70 | 0 / 0 |
The oldest live-guest sighting was 137–152s on `main` and at most 21s
with the fix.
Not in this change: shared `cluster` storage (`ceph-rbd`, `cephfs-data`)
and the mock Ceph cluster are never re-sighted by any cohort, so they
read stale on both builds at any tick rate. That needs its own fix.
The mock update loop selects PVE inventory by node name, one cohort at a
time. Shared storage merged under a cluster-level owner ("cluster" for
the PBS stores, "shared" for shared-storage, ceph-rbd and cephfs-data)
and the mock Ceph cluster match no node, so the cohort loop never
re-sighted them. They kept their startup stamp and read as stale Proxmox
sources within minutes of start, on every tick rate. The Ceph cluster
was worse: its LastUpdated was set from the wall clock at generation and
never touched again, even by the full refresh path. The public demo's
Storage and Ceph views show this.
Sight cluster-scoped inventory once per PVE cohort rotation, the cadence
mock.ProxmoxRefreshInterval reports and the mock Proxmox stale threshold
derives from, while any of its reporting nodes (or any node of its
instance when none are listed) is online. The same selection now
resamples shared storage usage, which the cohort loop also skipped.
Mock-only: the real storage poll stamps cluster storage with the poll's
start time on every poll, and pollCephCluster sets LastUpdated each
poll.
The mock update loop sights PVE nodes and their guests through ten fixed
cohorts, sized for the default two-second tick (every node within 20s).
The count did not follow PULSE_MOCK_UPDATE_INTERVAL, so the public demo's
15s tick sighted each node's guests only every 150s while the registry
judges Proxmox sightings stale after 60s. Within minutes most running
guests read as warning/telemetry_stale with proxmox source "stale", and
the Proxmox overview's Attention chip climbed to 64 of 77 guests.
Rotate the PVE estate through the largest divisor of the cohort count
that still sights every node within 30s, down to every node per tick. A
divisor keeps the loop's cohort index mapping each node to one tick per
rotation, and per-second churn never exceeds the two-second default. For
ticks slower than that budget, derive the mock Proxmox stale threshold
from the actual sighting cadence (mock.ProxmoxRefreshInterval), never
below the configured PVE derivation, the same way 1d9e66b24c derived the
supplemental provider thresholds.
Mock-only: real PVE polling rebuilds every guest of an instance with the
poll's sample time, and its threshold already derives from the poll
interval or the adaptive scheduler's maximum.
The German and Spanish getting-started guides told paid customers to
activate under Settings -> Plans -> Existing purchases. That heading
left the app in June (27ba85ddc6); the key field now sits under
Settings -> Plans & Billing -> License recovery, so customers following
the guide looked for a section that does not exist. Update both guides,
their shipped mirrors, and the machine-facing string the localized docs
test pins.
The one-time "Hover a guest to preview history" tip in the guest
toolbar had no colour class, so it rendered in body-text white while
every control around it (View, Add filter, the History label) is muted
slate. A passive tip outshone the controls and read as a stray label.
It now uses the muted token, so it reads as a caption for the History
range control it explains. Reveal, clear-on-hover and phone hiding are
unchanged.
The README workspace capture and social preview are refreshed from
the same build so they show the muted hint.
Contract-Neutral: Hint colour token only; no change to what the workloads toolbar shows or when
The README and SCREENSHOTS captures dated from 6 August. Since then the
Proxmox overview moved the node table above the guest filters and gained
the history range picker, and the Patrol page was rebuilt around a
decision inbox with Protection and Activity tabs, so the Patrol image
showed a page that no longer exists.
The new captures come from a main build running the public demo's mock
estate, at 2x for high-density screens, and the social preview reuses
the new workspace capture. SCREENSHOTS.md no longer gives demo
credentials because the demo signs visitors in automatically.
Release candidate cut from main at 045e7af651. It carries every change merged since stable v6.5.0.
Contract-Neutral: Release-only v6.6.0-rc.6 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
## Why
After #3093, the remaining weight of the alert history fold is evidence
nothing reads. On the long-running dogfood install, a resolved snapshot
averages 21 KB, and 83% of that is `evidence`: a trail of up to 64
envelopes (35 on average), across 1,895 occurrences kept for 90 days
(#1803).
Nothing in Pulse reads `Alert.evidence` from history. The incident
panel's evidence comes from the incident timeline endpoint, the AI's
alert history uses recently resolved alerts, and the attention summary
and quality telemetry never touch it.
## What changed
- The fold trims each decoded snapshot to its newest envelope, by
`observedAt` and then position. An updated envelope keeps its slot, so
the last one isn't always the newest. Merging takes the incoming
snapshot's evidence, so the fold never holds a full trail.
- History output applies the same rule to live overlays and the JSON
fallback, so every `GET /api/alerts/history` entry carries at most one
envelope.
- `operationalRecord.evidenceIds` and `transitions[].evidenceIds` keep
their full lists. The event log keeps every snapshot whole, and `GET
/api/alerts/active` still returns an open alert's full evidence.
- The api-contracts history clause, `docs/API.md` (and its shipped
mirror) and the alerts contract say so.
This is an intentional change to a public API response, approved by the
maintainer. A client that read full evidence from history should read
the active alert, the incident timeline or the event log.
## Measured
On a log shaped like the dogfood install's (1,900 occurrences, 35
envelopes each), the history fold retains 2.1 MB of heap, down from 27.2
MB.
## Checks (pulse-dev)
- New tests:
- newest by time and tie-breaking;
- the fold holds one envelope while the record keeps all 64 IDs;
- the event log stays whole;
- live overlay and JSON fallback;
- direct output shaping, with its input untouched;
- `docs/API.md` states the rule.
- Negative controls: keeping full evidence in the fold, in the output,
or choosing by position each fail their test.
- `internal/alerts/...`, `internal/api/alerting/...` and
`docsLinks.test.ts` pass. Prettier, vet, the completion, contract,
docs-mirror and browser-verification guards all pass.
Related to #1803
## Problem
Docker and Podman container updates need an agent with command access.
The only way to grant it was a checkbox called **Enable legacy combined
command profile**. It sat behind **Show advanced connection and install
options**, which only appears after the install token is minted. The
word "legacy" told the users who want updates not to use it. In #1728 a
user missed it entirely and hand-edited token scopes instead.
## Is the option being retired for container updates?
Not yet, so it should not be labelled as if it were.
- `docker_container_action_executor.go` sends updates through the agent
command session.
- The separate action runner implements `container.update`, but
`AGENT_SECURITY.md` lists its container actions as unqualified.
- The runner needs the safe collector profile. Under that profile,
rootful Docker gets summary inventory only, with no update checks or
actions.
The security posture is unchanged. Command access is off by default.
Choosing it mints an execution-scoped token and records
`command-capable`. A `monitoring-only` install still cannot be promoted
remotely.
## Changes
- The checkbox now reads **Allow Pulse to run commands on this host**
and sits in the token step, so the choice is made before the token is
minted. Its description says it is needed for container updates, start,
stop and restart, and Patrol actions. It also says Pulse can then run
commands as root, and that the option cannot be switched on later from
Pulse.
- The token summary no longer claims command actions can be enabled per
host from Settings. It now states the real choice.
- One shared label (`getUnifiedAgentCommandAccessOptionLabel`) feeds the
installer, the locked Docker update pill and the Proxmox setup guide, so
their wording cannot drift apart.
- `DOCKER.md`, `AGENT_SECURITY.md` and `PRODUCTION_SECURITY.md` name the
option the same way.
## Verification
- Private mock stack, 1400 px and 375 px:
- The option appears in step 1 before a token exists.
- Ticking it and then generating a token mints with command execution,
and every install command carries `--enable-commands`.
- Unticking it regenerates the token without that permission.
- The advanced options no longer hold it.
- The Docker and Proxmox notes name it.
- Mock Docker hosts carry no agent facet, so the "installed without
command access" pill cannot render on mock data. Its copy is covered by
the `containerUpdateBadgeModel` and `ContainerUpdateBadge` tests.
- Settings, shared components, Docker, the container drawer and utils
suites pass: 4436 tests on a rerun. The first run on a heavily loaded
pulse-dev had one unrelated failure, and the rerun passed in full.
Typecheck, ESLint and Prettier are clean.
- Browser receipt included. Contract notes added for agent-lifecycle,
api-contracts and frontend-primitives.
Most of the alert history fold was evidence nothing reads. On the
dogfood install a resolved snapshot averaged 21 KB, 83% of it the
evidence trail of up to 64 envelopes (35 on average), across 1,895
occurrences kept for 90 days (#1803). No Pulse UI, AI tool or API
handler reads Alert.evidence from history: the incident panel uses
the incident timeline, and the AI uses recently resolved alerts.
The fold now trims each decoded snapshot to its newest envelope, by
observedAt and then position (an updated envelope keeps its slot, so
the last one is not always the newest). History output applies the
same rule to live overlays and the JSON fallback, so every
/api/alerts/history entry carries at most one envelope.
operationalRecord.evidenceIds and transitions keep their full lists,
the event log keeps every snapshot whole, and /api/alerts/active still
returns an open alert's full evidence.
On a log shaped like the dogfood install's, the history fold went from
27.2 MB to 2.1 MB of retained heap. Negative controls: keeping full
evidence in the fold, in the output, or choosing the newest by position
each fail their test. The alerts tree passes on pulse-dev.
Related to #1803
## Why
A heap profile of the long-running dogfood install (77.6 MB live after a
forced GC) shows the alert history projection holding 36 MB (47%). It
keeps one merged alert per occurrence for the event log's 90-day
retention: 1,895 occurrences there, with resolved snapshots averaging
about 20 KB. Reads made it much worse (#1803):
- every history read deep-copied the whole projection before applying
its limit, so the attention summary's 500-row read copied all 1,895
occurrences;
- every read with a `since` (the 30-day alert-quality window,
date-filtered history) built a fresh projection and re-decoded every
snapshot in its window.
That churn is why the same install holds about 158 MB of Go heap for 78
MB of live data.
## What changed
- One full-history fold, built and extended as before. Each occurrence
also keeps a small mark per folded event (time, durable ID, and the
`LastSeen` it contributed) and the index of its latest acknowledged
snapshot.
- A `since` read is answered from that fold. The window starts at each
occurrence's first event that the log's own `occurred_at >= since` text
comparison admits. `LastSeen` and acknowledgement come from that point
on, and occurrences are ordered by their first in-window event, exactly
as a walk of only the window would produce.
- Overlay, sort and limit run on lightweight rows under the projection
lock. Only returned rows are copied, once, by
`canonicalizeAlertHistoryForOutput`.
- The alerts contract notes the windowed-read rule and the pinning
tests.
## Measured (pulse-dev, log shaped like the dogfood install's: about
1,900 occurrences, 40k events)
| Read | main | this PR |
|---|---|---|
| 30-day window | 68.9 ms, 19.8 MB allocated | 5.2 ms, 2.8 MB |
| Latest 500 | 12.7 ms, 8.5 MB | 3.6 ms, 2.0 MB |
| Full history | 20.8 ms, 12.9 MB | 13.4 ms, 6.9 MB |
The 36 MB the fold itself holds is unchanged. The new marks add about 56
bytes per lifecycle event, about 2 MB at 40k events.
## Checks
- `TestWindowedHistoryMatchesAWalkOfOnlyTheWindow` compares every window
and limit against an independent walk of only the window (the original
algorithm). It covers 25 random histories with acknowledgements on both
sides of the boundary, out-of-order `LastSeen`, uneven fractional
seconds where RFC3339Nano text order and time order disagree,
same-instant events, a history clear, and live overlays.
- `TestWindowedHistoryReusesTheFullHistoryFold`,
`TestLimitedHistoryReadCopiesOnlyReturnedRows` and
`TestHistoryReadResultsDoNotAliasTheSharedFold` pin reuse, copying and
isolation.
- Negative controls: ignoring the window, building a fresh fold per
`since` read, and copying every row before the limit each fail their
test.
- Removing the per-row clone did not fail the aliasing test, because the
output copy already isolates results. That double copy was dropped, and
the test guards isolation end to end.
- Vet and the whole `internal/alerts/...` tree pass on pulse-dev.
## Not established
How much held heap this saves on the dogfood install itself. That needs
this build running there.
Related to #1803
The only way to give an agent the command access that Docker and Podman
container updates need was a checkbox called "Enable legacy combined
command profile", behind "Show advanced connection and install options",
which only appeared after the install token was minted. The name told the
users who want updates not to use it, and #1728 shows a user missing it
entirely and hand-editing token scopes instead.
It is not being retired for container updates yet. Updates dispatch
through the agent command session; the separate action runner's container
actions are unqualified, and rootful Docker under the safe collector
profile has no update checks or actions. So the option stays, with the
same security posture: off by default, an explicit choice mints an
execution-scoped token and records command-capable, and a monitoring-only
install still cannot be promoted remotely.
The checkbox now reads "Allow Pulse to run commands on this host" and sits
in the token step, so the choice is made before the credential is minted.
Its description says what it enables, that Pulse can then run commands as
root, and that it cannot be switched on later from Pulse. The token summary
no longer claims commands can be enabled per host from Settings. One shared
label feeds the installer, the locked Docker update pill and the Proxmox
setup guide, and the docs name the option the same way.
Go 1.26.10 (20 October) distrusts the compromised eMudhra intermediate
CAs in `crypto/x509` (CVE-2026-111943). Pulse binaries built with 1.26.9
still accept chains through those intermediates on every verified
outbound TLS connection. govulncheck does not report a trust-store
change as a called symbol, so the scheduled scan stayed green.
This moves the governed toolchain floor and every pin together:
- `go.mod`, `scripts/.go-version`, the toolchain installer and the
release-script guards
- the source-built container stages (`Dockerfile`, provider control
plane, integration mock), using the `golang:1.26.10-alpine`
manifest-list digest, plus the Debian dev container
- the root-pair diagnostic workflow and script, and their tests
- the deployment-installability contract floor
It also moves Vite from 6.4.3 to 6.4.5, which fixes the three
`launch-editor` advisories behind today's failed dependency advisory
watch and adds the 6.4.5 `server.fs` query check. Both affect only the
local dev server. The lockfile changes only the `vite` entry, and
`dependencySecurity.test.ts` now pins the floor.
Checked on pulse-dev against this exact tree: the go.mod toolchain
resolves to go1.26.10, `go build ./...` succeeds, the root-pair
diagnostic tests pass, govulncheck is clean, the dependency floor test
passes, `npm audit` reports 0 vulnerabilities, and `vite build`
succeeds. `scripts/installtests` passed except
`TestAutoUpdateRunningExecutableIdentity`, which hit its 15-second
subprocess deadline while pulse-dev was at a load average of about 20.
Rerun alone, it passed 3 out of 3 times on this tree and failed 3 out of
3 times on unchanged `origin/main` under the same load, so the timeout
is load-sensitive and not caused by this change.
On a long-running install the alert history projection was the largest
part of the Go heap: 36 of 78 MB live on the dogfood install, one merged
alert per occurrence for the event log's 90 days (#1803). Every read
then deep-copied the whole projection before applying its limit, so the
attention summary's 500-row read copied all 1,895 occurrences, and every
Since read (the 30-day alert-quality window, date-filtered history)
built a fresh projection and re-decoded every snapshot in its window.
Each occurrence now keeps one small mark per folded event (time, id and
the LastSeen it contributed) and the index of its latest acknowledged
snapshot. A Since window starts at the first event the log's own
occurred_at >= since text comparison admits, takes LastSeen and
acknowledgement from there and orders occurrences by their first
in-window event, so it equals a walk of only the window. Overlay, sort
and limit run on lightweight rows under the projection lock, and only
returned rows are copied, once, by canonicalizeAlertHistoryForOutput.
On a log shaped like the dogfood install's (about 1,900 occurrences,
40k events), a 30-day read went from 68.9 ms and 19.8 MB allocated to
5.2 ms and 2.8 MB, a 500-row read from 12.7 ms and 8.5 MB to 3.6 ms and
2.0 MB, and a full read from 20.8 ms and 12.9 MB to 13.4 ms and 6.9 MB.
A randomized test holds every window and limit equal to an independent
walk of only the window, across acknowledgements on both sides of the
boundary, uneven fractional seconds, same-instant events, a history
clear and live overlays. Negative controls: ignoring the window, a
fresh fold per Since read and copying every row each fail their test.
Related to #1803
Go 1.26.10 (20 October) distrusts the compromised eMudhra intermediate
CAs in crypto/x509 (CVE-2026-111943). Binaries built with 1.26.9 still
accept chains through them, so every verified outbound TLS connection
Pulse makes (licence, updates, webhooks, AI providers) stays open to
interception by a holder of those keys. govulncheck does not flag a
trust-store change as a called symbol, so the scheduled scan stayed
green while the gap existed.
Advance the governed toolchain floor and every pin together: go.mod,
the toolchain installer and release-script guards, the source-built
container stages (manifest-list digests for golang:1.26.10-alpine and
the Debian dev container image), the root-pair diagnostic, and their
tests.
Vite 6.4.3 bundles a launch-editor with three advisories
(GHSA-587h-7wmj-h5pm, GHSA-r825-w8mm-5pxw, GHSA-698v-4r3x-3vcv) that
failed the daily advisory watch. 6.4.4 bundles the fixed launch-editor
and 6.4.5 adds the dev-server server.fs query check. Both reach only
the local dev server, not the built product. The lockfile changes only
the vite entry, and dependencySecurity.test.ts now pins the floor.
The Docker table offered a live Update button for containers whose update
the server had already refused, most often because the host's agent was
installed without command access. The click then failed with "command
agent is not connected", so on such hosts the feature never worked and
never said why.
The server's refusal arrives only over REST. Opening the Docker page from
another platform page seeded its cache from the websocket-fed
all-resources cache, which read as fresh, and websocket ticks kept it
fresh, so the page never fetched REST and never learned of the refusal.
Each cache entry now records when it last received a REST snapshot, and a
prefer-ws-then-rest route revalidates at mount when that snapshot is
missing or stale. The table also builds each row's update action
reactively, so readiness that arrives after mount reaches the row.
The update control now requires the server to advertise the update
capability, so a host with stale inventory no longer offers it either.
The locked pill reads "Update" to fit the column and explains the cause:
for an agent without command access it names the host and the install
option that grants it.
Locally built images have no RepoDigest, so no registry holds them. The
agent asked Docker Hub anyway and reported its 401 as "Check failed" on
every cycle. It now reports a "local image" state without a registry
request, shown as Local beside Pinned.
Contract-Neutral: performance-and-scalability gains one mock fixture state with no hot-path change; storage-recovery routes keep REST-first hydration, which this change does not alter.
Promote v6.6.0-rc.5 to stable after its 24-hour soak. Only release metadata changes.
Contract-Neutral: Release-only v6.6.0 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
Five stores set private page caches of 16 to 64 MB per connection.
The modernc driver allocates that cache with anonymous mmap outside
the Go heap, so GC and GOMEMLIMIT never see it, and each page costs
about twice its size. Measured: reading a 107 MB database through a
64 MB cache_size added 117 MB of resident memory, against 6 MB at
SQLite's 2 MB default. On a long-running dogfood install about 165 MB
of 360 MB resident sat outside the Go heap, with a 126 MB audit
database behind a 64 MB cache (#1803).
Audit, RBAC, notification queue, unified resources and the alert
event log (writer and both readers) now take pdb.PageCachePragma.
Pages that miss it come from the kernel file cache, which the kernel
can reclaim. The audit suite, including its large legacy migration,
takes the same time as before (309 s on both trees).
pkg/db tests reject any other cache_size in production code and bound
the resident growth of a 30 MB scan. Each store's test pins its
effective value. Negative controls: restoring the audit cache fails
the source scan, and a 64 MB reader cache grows a scan by 55 MB
against a 16 MB budget. modernc sorts _pragma values before applying
them, so a second cache_size in one DSN cannot reliably override.
Related to #1803
The Pro comparison card on Plans & Billing was a half-width box of small grey text: no price, every feature at the same weight, and a small outline link as the only way to buy. It now has a header with the plan name, a one-line tagline, the price and a primary View plans button, then the features in three labelled groups with check marks: for your team, for larger estates, and Patrol and history.
The in-app Pro comparison card, now shown to free installs, led with Patrol modes and 90-day history: value an individual needs for their own setup, which reads as a withheld free version (issue #1409 called the earlier caps greedy). Paid features for an organisation read as normal, and paying installs skew to larger estates (45% against 8.5% of free installs). The card now leads with team controls, reporting and agent profiles, external probes and support, with Patrol modes, Patrol fixes and 90-day history after. Prices and entitlements are unchanged.
A free self-hosted install had no way to buy from inside Pulse. Plans &
Billing was unlisted, the plan page showed only the current plan unless an
upgrade prompt had sent the operator there, and those prompts are hidden on
free installs. Plan-locked lines in Settings said "Higher license plan
required" without saying where the plan lives.
Plans & Billing is now listed for every install that does not hide
commercial surfaces, and its plan page shows a free install the Pro
comparison and its purchase action. Plan-locked lines in Settings link to
it. Proactive surfaces stay quiet: no badges, banners, trial prompts or
upgrade calls to action on monitoring pages, and demo and white-label
runtimes still hide everything commercial.
Purchase start also failed for plain-HTTP installs on a LAN address, the
default homelab setup, because there is no secure address to return the
signed purchase token to. Those installs now get a handoff that finishes
on the website, which shows and emails the activation key; no return token
is minted or sent over plain HTTP.
Every Buy, Upgrade and Renew action opened the Pulse Account portal on cloud.pulserelay.pro, which has returned a Cloudflare 522 since hosted Cloud was hibernated on 2026-06-01, so no one could buy from inside Pulse. The licence server now serves the same handoff-to-checkout page at https://license.pulserelay.pro/buy, using the existing portal handoff and checkout session APIs, so payment still returns to the instance and activates it.
Release candidate cut from main at da0d19cd60. It carries every change merged since stable v6.5.0.
Contract-Neutral: Release-only v6.6.0-rc.5 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
Release candidate cut from main at 1c2f23aef6. It carries every change merged since stable v6.5.0.
Contract-Neutral: Release-only v6.6.0-rc.5 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
Retain the resource projection source checks and strict two-CPU chart
measurement with exact source, control and executable identities, complete
capacity receipts and earlier adverse outcomes. Keep the 90 ms budget and
the broader performance lane residual unchanged.
Raw fleet reads copied resource IDs across SQLite for every observation and
merged tier branches by those repeated strings. Bind the requested scope once,
return statement-local resource numbers and restore each detached series to
stable timestamp order in the shared reader. Preserve indexed tier coverage,
metric filters, chunk limits and ordered streamed display buckets.
Bind the source tier and time window once for both clustered rollup stages.
Reusing the ordinary aggregate's six SQLite slots removes the duplicated
argument array and boxing that exceeded the small-rollup allocation budget.
Preserve indexed ranges, aggregation results and the legacy query path.
Use the covering time index to discover resources in each rollup window,
then aggregate their contiguous primary-key ranges. Group on a query-local
resource number to avoid repeating composite identity comparisons for every
point. Preserve the legacy aggregate until its physical migration completes.
Keep nullable extrema, bucket boundaries, resource identities and idempotence
equivalent to the legacy aggregate, and guard both indexed query stages.
Retained chart reads found each sample in the identity index and then fetched its payload from the rowid table. Store observations in the time-major primary table to remove that second lookup while preserving sample identity and retention.
Migrate installed stores through deferred atomic WAL maintenance, preserving old read snapshots, queued writes, nullable bounds, tier reconciliation and rollup metadata. Share the rewrite deadline with auto-vacuum and retain the installed schema when capacity or migration checks fail.
Keep the existing write ceiling, query-cache bounds, chart sampling and 90 ms SLO unchanged. Qualify migration cancellation and crash recovery, cross-Store cached reads, restart and production checkpoint behavior.
Hosted measurements had competing CPU activity and could not establish the
existing 90 ms local SLO. Give the focused disposable-worker route explicit
L10 ownership and source-guard verification without changing qualification
budgets or closing the broader performance follow-up.
Remove unnecessary retained-read sorting, duplicate extrema decoding and
chart point copies while preserving canonical samples and full extrema APIs.
Isolate guest HTTP test fixtures from earlier endpoint cooldowns.
Keep the idle-worker 90 ms verification and existing headroom follow-up open.
Reuse the existing per-case endpoint namespace helper for guest HTTP-status
tests so a recycled loopback address cannot inherit another fixture's
intentional uncertainty fence. Keep diagnostic and cluster clients within
each fixture on the same identity, preserving all runtime cooldown rules.
A shared-listener control reproduces the old bare-endpoint failure and
verifies independent first commands plus unchanged earlier fences. Targeted
and full Proxmox tests pass. Record this separate test-stabilization slice.
Use the canonical timestamp/value model directly from the shared retained
read engine. Preserve the full extrema reader, tier reconciliation, Docker
observation provenance, filters and chart sampling while eliminating unused
column transport and a second series copy.
Verify both projections, detached reads and chart sampling equivalence.
Record the measured memory reduction and retain inconclusive timing and
failed capacity evidence. The idle-worker 90 ms proof remains open.
Keep absent bounds nullable across the SQLite row boundary and apply their
existing value fallback during decoding. Preserve explicit bounds and the
same fallback before SQL and streamed display aggregation.
The full metrics suite passes. Ten alternating diagnostic samples reduce
allocations by 24.59% and bytes by 4.24%; their latency difference remains
inconclusive. Preserve the strict candidate failure and competing-capacity
receipt without claiming a qualified 90 ms result.
Contract-Neutral: Retained chart values, extrema, tier reconciliation, sampling and cache boundaries remain unchanged; only decoding allocation cost and capacity-error reporting change.
Execute the fixed candidate directly at the existing 90 ms budget. Keep
relative comparisons in the paired benchmark job so an invalid baseline
cannot suppress the candidate check.
Retain per-process CPU and measurement I/O counters. Require quiet I/O
before admission while counting the test's own reads in its latency.
Preserve the rejected hosted measurement and inconclusive tier-pruning
experiment as adverse evidence.
Contract-Neutral: Only verification harness and evidence change; chart payloads, retained reconciliation, latency budgets and required source/release gates are unchanged.
Stream ungrouped retained reads in the existing time-major index order,
avoiding temporary metric sorts without changing tier reconciliation or
chart samples. Extend query-plan and filtered-result coverage.
Add an explicit strict mode for the existing 90 ms API budget and an
optional fresh-worker CI proof with fixed executable identities and raw
capacity evidence. Keep ordinary hosted budgets and required gates intact.
Contract-Neutral: Chart payloads, retained-tier reconciliation, sampling, index layout and source/release gates are unchanged; the implementation removes an unnecessary sort and adds controlled verification of existing latency budgets.
Bind the completed source blocker to its exact checks and protected-main
landing. Keep release inclusion, installed recovery and the existing manual
reliability exit decision explicit without changing priority or lane state.
Keep monitoring readings, trusted connections and recovery guidance
reliable when observations are stale, guest responses are incomplete or
agent recovery is interrupted.
Qualified source: `b6e56c5baa2ad995264faa80877543300bc10273`. All nine
[required source
checks](https://github.com/rcourtman/Pulse/pull/2888/checks), the
[native
matrix](https://github.com/rcourtman/Pulse/actions/runs/39148879516) and
[Core E2E](https://github.com/rcourtman/Pulse/actions/runs/39148879470)
passed. Intel accounted for all 545 installer roots, with 532 passes, 13
explicit platform skips and no failures. Its remaining installer phase
passed in 18m48s, followed by native agent build and execution. The
matrix covers Linux x64/ARM64, macOS Intel/ARM64 and Windows x64
execution plus the FreeBSD cross-build contract.
- Preserve reading provenance, measured zero, stored History and VM/LXC
memory interpretation. Selected-agent staleness cannot renew a retained
observation or invalidate independent provider readings.
- Reject incomplete or foreign guest responses and retain saved
certificate trust, scoped access, private installer input, native NAS
teardown and bounded recovery guidance.
- Keep initial Proxmox login within its caller and total client timeout
while accepted-session renewal remains shared. Preserve the PBS owner's
context outcome when cancellation races a response, prevent a cancelled
fallback and reject late session publication.
- Assert troubleshooting workflows and destinations semantically. Run
every discovered Python safety control and native Go root independently
with existing fixture, operation, root and job deadlines. Script-smoke
and native verification share one control suite, including real
compiled-Go failure, exit-zero, timeout and exact-selector controls.
- Integrate current main while retaining selected-agent freshness, guest
memory interpretation, native GNU prerequisites, API concurrency
controls and both parents' subsystem contracts.
Worker proof passed 113 focused frontend cases, API and guest-transport
race checks, all 59 Python fixture controls, production frontend build
and full frontend lint/types. The exhaustive worker installer inventory
accounted for 551 roots: 535 passed and 16 explicitly skipped for
platform reasons. The eight consolidated runner controls passed through
the script-smoke harness and native Go wrapper. Authentication proof
passed all 340 Proxmox tests, 108 PBS tests and the affected monitoring
cancellation controls under the race detector. The new PBS test first
failed all ten JSON/form cases on unchanged runtime, then passed with
the correction. Status, ownership and normal commit checks passed.
Browser proof uses production components and CSS with synthetic
observations and endpoints. Drawer History passed at 1280×900, 1024×768
and 390×844. Private installation passed at 1280×900 and 390×900 in both
themes. It covers stale/current/unknown readings, provider caveats,
stored History and keyboard inspection, wrapped signing keys, token
copy/dismissal and Last seen advancing without another lookup. Final
screenshots were inspected, the source-bound receipt covers the combined
runtime files, and owned browsers and servers were closed.
Retain the [previous Frontend
assertions](https://github.com/rcourtman/Pulse/actions/runs/38660106123/job/116179237804),
[Intel
timeouts](https://github.com/rcourtman/Pulse/actions/runs/39138980077/job/117709536519),
[script-smoke integration
failure](https://github.com/rcourtman/Pulse/actions/runs/39145479921/job/117728549560)
and [PBS cancellation
race](https://github.com/rcourtman/Pulse/actions/runs/39145479921/job/117729442632)
with their diagnosed causes and corrected proof. The final matrix above
qualifies the combined corrections.
The [earlier advisory benchmark
comparison](https://github.com/rcourtman/Pulse/actions/runs/39138980114/job/117709718583)
reported three legacy history-filter regressions. A worker repeat
reproduced one. Longer controls showed wide timing variation even
between byte-identical binaries. The [45a82ea03c
comparison](https://github.com/rcourtman/Pulse/actions/runs/39145479921/job/117728549510)
passed against a newer baseline. The [final
comparison](https://github.com/rcourtman/Pulse/actions/runs/39148879492/job/117738375776)
completed execution but failed on test-only empty/legacy filtering,
2.945 to 3.263ns, +10.82% (p<.001, ten samples). Production empty
filtering improved 16.70%, interleaved filtering rose 2.96%, and history
allocation counts were unchanged. The filter and benchmark sources are
byte-identical across the final comparison. This does not establish a
cause or performance closure. Artifact 12145720151 retains full paired
samples, comparison and metadata. The overall Build and Test workflow is
red from this advisory job while all nine protected required checks
pass. The existing L1 reliability follow-up owns repeatability and
attribution. No benchmark threshold, protected check or release gate
changed.
Main's benchmark/SLO test and documentation changes in PR3053 compose
without conflict. The candidate benchmark artifact records synthetic
merge c21333c676ac004935c9683027490203d8f869ea with tree
fa059ae37b, combining this head and main
52b40fd611. A bounded worker composition
proof also executed all three chart benchmark cases under the race
detector with source hashes unchanged. Its SLO test explicitly skipped
under the race detector, and one-iteration execution supplies no
performance comparison.
This proposal qualifies source delivery. Release inclusion, installed
upgrade/recovery, guest thaw, fresh writes on every backed-up filesystem
and workload liveness remain separate outcomes. Reliability keeps its
existing priority. Recovery/change feature expansion stays deferred.
Related to #1099, #2439, #2619, #2822, #2897, #2757, #2511, #2513,
#1788, #2755 and #2140.
A cancelled login can consume an empty response before the transport reports
cancellation. Preserve that context outcome for the owner and joined readers,
reject late session publication and prevent a cancelled refusal from retrying
another encoding. Keep live response validation and later recovery intact.
Script smoke still asserted the removed grouped runner while native
verification used a separate suite. Share the canonical execution controls
so both entrypoints reject lost checks, false passes and changed deadlines.
The chart benchmark warmed its response cache and then timed a fresh
payload-retaining recorder. That hid store-backed work behind test buffer
allocation and made the regression signal ambiguous.
Measure cached handler work, fresh-service computation and real loopback
HTTP transfer separately. Wait for store startup maintenance, preserve the
original failed comparison and record the paired diagnostic limits. Make
the adjacent SLO exercise uncached chart generation at its existing budget.
Refs #3052
Preserve the landed L6 source, response-verdict controls and ownership evidence
alongside the maintenance source repairs. Final protected checks and native
qualification must cover this combined source before landing.
Compile and discover one exact test binary per native workflow phase, then
execute every discovered root check independently. This removes inventory
growth from another check's ten-minute Go alarm while preserving fixture,
installer and overall CI job deadlines.
Retain exhaustive phase coverage and reject missing completion, failed checks,
unexpected exit zero and timeouts using real compiled Go execution controls.
Register the runner with deployment installability and retain the failed Intel
and advisory benchmark evidence in the existing reliability record.
Initial password authentication has one owner and must end with its
constructor. Keep its complete JSON/form exchange within the caller and
client timeout. Retained-session renewal still serves concurrent readers
independently of any one caller's cancellation.
Retain generation-based coalescing and the shared parser, exercise both
initial encodings under the client timeout, and document the ownership
boundary alongside existing startup and cluster cancellation controls.
Retain the failed CI and reproduced baseline in the existing reliability
record while final-source qualification remains open.