The Repair Authentication command generated for Unix agents carried
--update and a fresh token but no identity, while the Windows path pins
PULSE_AGENT_ID and PULSE_HOSTNAME. A repair reinstall without the pin
can register a fresh suffixed agent identity for the same machine
instead of converging on the one being repaired, which is exactly what
a field report documented across repeated repair attempts.
Pass --agent-id and --hostname from the canonical connection the same
way the Windows command does.
Refs discussion #1748
Contract-Neutral: Discussion #1748: unix repair command now pins the existing agent identity, parity with the windows path; command-string generation only, no API or payload change
The alert history FilterBar rendered bare Critical/Warning chips while
the platform tables' status chips now carry counts. Expose
countForSeverity from useAlertHistoryState, built on the same
filterAlertHistoryItems predicate the list renders through, so each
severity chip shows the row count its selection yields for the fetched
period and current search. Counts respect the shared Inventory totals
visibility preference; the Period facet stays uncounted because it is a
time scope, and only the currently fetched range is available to count.
Only two platform tables ever adopted the attention-count affordance, so
Docker, Kubernetes, TrueNAS, Machines, and the Proxmox detail tables all
rendered bare status chips while the Proxmox and vSphere workloads bars
showed counts. Replace withPlatformAttentionCount with
withPlatformStatusCounts and roll it across every PlatformTableToolbar
status facet: each chip now carries the row count its own selection
would render, sourced from the same predicate the table filters with
(createPlatformTableFilterState.countForStatus, or the surface's
governing filter on the bespoke Docker storage, Kubernetes section, Ceph
and Replication toolbars), so a chip's number can never disagree with
the rows clicking it shows. Counts respect the existing estate-wide
'Inventory totals' visibility preference, the attention tables keep
their tone escalation, and the container-attachment options drop their
static ariaLabels so the count is part of the accessible name.
The Proxmox and vSphere overview filter chips counted the estate-wide
workload inventory while the table below applied the page's platform
scope, so a degraded workload on another platform (a Pending Kubernetes
pod) produced Attention 1 whose click rendered 'No guests found'. A
forced platform is the page's identity, not a user filter: apply it in
selectVisibleWorkloadInventory before dedupe so chip counts, search
suggestions, and availability detection describe the same guest set the
table can ever render.
The unified agent's Linux installer only offered the root profile, and the
docs called non-root unsupported. That default is the most-cited reason
security-conscious evaluators reject Pulse without trying it. install.sh
gains --least-privilege: the service runs as a dedicated nologin
pulse-agent system user with every existing hardening directive, no
LXC-attach ambient capabilities, docker-group membership for socket reads,
and refusal (not silent root fallback) on appliance platforms, non-systemd
init systems, and --enable-commands. Optional --grant-smart and --grant-pct
restore the only two collectors that need elevation through
visudo-validated exact-command sudoers rules and root-owned wrappers the
agent reaches via new absolute-path-only PULSE_SMARTCTL_PATH /
PULSE_PCT_PATH overrides; the pct grant covers pct list and pct df only
and can never widen into pct exec. --update preserves the profile and its
grants by reading the installed unit.
The agent now authors a privilege block in its report (effective root,
service user, active helpers), carried through models into the fleet
doctor as a descriptive field: Agent Doctor shows the profile and its
helpers instead of presenting intentionally absent collectors as a fault,
and a least-privilege agent can never be marked unhealthy on that evidence
alone.
The delivery health verdict said when something was wrong; nothing showed
what actually fired and where it went. Expose the queue's retained
per-attempt audit rows as GET /api/notifications/delivery-log (newest
first, retention-labeled, webhook secrets redacted from error text) and
render them as a Recent delivery activity card on the alert destinations
tab, with outcome badges, destination names, and failure classes. Audit
rows now persist the normalized destination identity in a destination_id
column; older rows fall back to their operational links.
Test sends bypass both the queue and the activation gate, which is exactly
how installs came to believe delivery worked while every real alert was
suppressed (483 installs in the 08-18 telemetry read). Successful test
responses now carry deliveryPaused: true whenever the manager is gated
off, and the destinations UI warns instead of celebrating.
The de translation landed with a semicolon inside user-visible copy, which
the copy-style audit rejects, so every subsequent commit on main was
blocked. Split the sentence instead; wording is unchanged.
Contract-Neutral: copy-style punctuation fix in existing de translation string; no contract, payload, or behavior delta
Discovering Docker inside Proxmox LXC guests previously required an
undocumented environment variable plus a hand-edited systemd unit,
which is how the surface kept shipping invisible. The opt-in is now a
persisted system setting with an admin-only toggle in Settings →
System → General, presented as its own "Docker in Proxmox LXCs" card
under the existing Docker/Podman updates card, with the requirements
(node agent with command execution) and data-collection bounds stated
inline.
The setting rides the admin-gated system settings endpoint
(RequireAdmin + settings:write, matching every other server-wide
toggle). Flipping it persists first, then applies to the runtime
config and fires a reconfigure hook so the router rebuilds the
monitor's Docker checker and inventory collector immediately — no
restart. PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY remains
authoritative: when set it locks the toggle (EnvironmentLockBadge in
the UI, HTTP 400 from the API), and an unrelated settings save can
never clobber the runtime value. The opt-in guardrail test now pins
the settings path, the env lock, and the persisted field.
Installer and node setup-guide callouts now point at the toggle first
with the environment variable as the locking override, and the docs,
their public mirrors, and the agent-lifecycle subsystem contract say
the same. New i18n strings shipped for en/de/es.
Verified live in mock mode: toggle off/on from the UI drives the
collector teardown/setup log lines, the value survives a server
restart, and all three touched surfaces render at desktop and mobile
widths (receipt in frontend-modern/browser-verification.json).
An in-progress PBS snapshot (reproduced on VM 117 at 9% written) was
counted as a completed backup: the guest's LastBackup jumped to "now" the
moment vzdump started, and the workloads badge showed a green healthy
shield for a backup that did not exist yet. If the job aborted, that
shield had never been true.
Detect the in-flight state from what the PBS listing actually says - no
size and no index.json.blob manifest in files - and flag the snapshot
InProgress instead of complete. On the PVE side, correlate storage content
entries with live vzdump tasks: a guest archive whose ctime falls inside a
running task's window is that task's partial output, and a PBS-storage
entry without a size has no manifest yet.
In-flight artifacts now feed a new per-guest BackupInProgress flag rather
than LastBackup, so the badge age always reflects the latest COMPLETED
backup. The workloads badge gets a distinct blue "Running" state (green
would claim success, None would hide that work is underway), with the
last-completed age kept in the tooltip and accessible label. Recovery
points map to OutcomeRunning with no completion time, keeping posture
freshness and the Backups page's latest-restore pointers anchored to
completed backups; the recoverable-artifact list shows a Running chip.
A cached in-flight snapshot forces a per-poll re-fetch of its group so
completion or abort is noticed promptly instead of after the cache TTL.
The pct exec socket probe that discovers Docker inside LXC guests
failed invisibly: errors logged at debug only, and a failed probe never
set DockerCheckedAt, so the guest stayed in first-check state and was
re-probed on every poll forever. On a node where lxc-attach could not
enter unprivileged guests that meant ~49 pct execs per minute of
steady-state churn, with nothing at default log level to say why the
Proxmox page showed no Docker.
Track consecutive probe failures per guest in the monitor. The first
failure of a streak, and the point where the backoff reaches its cap,
log at warn with the guest, node, and error; repeats stay at debug.
Failed probes retry with exponential backoff from one minute to a
thirty-minute ceiling, a success clears the streak with an info log,
and reconfiguring the checker resets all streaks so command execution
enabled from the UI at runtime still retries immediately. Entries not
refreshed for a day belong to deleted guests and are pruned.
PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS now gates the socket probe
as well as inventory collection. It previously only limited inventory,
so opting into specific guests still probed every running LXC on every
cycle; now guests outside the allowlist are never pct exec'd at all.
Subsystem contract and configuration docs updated to match.
The budget check had not run since 08-18 because the vitest step was
failing first. With the tests fixed it now flags a week of intentional
feature growth: the estate-overview and mobile table work grew every
major chunk (index, sharedPlatformPage, Alerts, Proxmox, AIIntelligence)
by 3-10 kB gzip, and removing the saved-views lazy boundary folded the
FormTextarea chunk into its importers. Rebaseline from a lockfile-clean
build; totals stay within the overall budget.
The estate-overview redesign folded attention banners and guest totals
into the canonical tables and the shared View menu, updating its own
co-located tests but leaving three pins in other files on the old
surfaces. Repin them: the Docker single-host View menu now legitimately
carries the inventory-totals toggle but still no layout choice, node
availability reads as the warning-toned ready/total ratio on the
Kubernetes cluster row, and Proxmox guest totals flow through the
toolbar inventory counts instead of a page-local memo.
Saved views stored a named URL query string in localStorage under
`pulse:filterbar:saved-views:<key>` and offered it back from a "Saved"
control on eight surfaces. A saved view was only ever the page's URL:
`saveCurrent` snapshotted `window.location.search` and `applyView`
navigated back to it.
The browser already does that, better. A bookmark syncs across devices,
survives clearing site data, is searchable, can be foldered, and can be
shared with someone else. The localStorage copy did none of those, so a
"saved" view silently vanished on a new browser or a cleared cache.
There is also no evidence anyone used it. It shipped 2026-05-01 in
v6.0.5 and no Pulse issue, comment, or discussion has mentioned it since.
#1510 is the telling case: filed two months after it shipped, by a user
who wanted their Proxmox status filter to survive a tab switch, which is
exactly the job this feature claimed. They did not reach for it, and the
fix that satisfied them was sticky route state (d20c556db).
Removed the hook, the menu, the `savedViewsKey` prop, and the per-surface
keys. URL-backed filters and sticky route state are untouched, so a
filtered page is still a shareable, bookmarkable link. On mobile, a
surface with no other auxiliary control no longer renders an action row
that existed only to hold the Saved button.
Subsystem contracts keep the URL-ownership rule this feature depended on
and now state it as the primary contract, with a note not to reintroduce
an in-app view library. A guardrail asserts the FilterBar barrel does not
re-export SavedViews.
User-visible removal: needs a release-note line at the next cut.
The helper added in 54f623cff omitted onPrimaryClick and onUtilityClick, so
tsc rejected the MobileNavBar element while vitest, which does not type-check,
still passed. Left main failing type-check.
The Assistant overlay and its backdrop reserved a hardcoded
calc(5rem + env(safe-area-inset-bottom)) for a bar that measures 45px. At
375x812 the backdrop ended at y=732 while the bar starts at 767, leaving a
35px band that was neither dimmed nor click-blocked: page content showed
through and stayed interactive outside an open modal.
The bar's height is content-driven and already includes the safe-area inset
via pb-safe, so no caller can hardcode it correctly. Five sites had tried:
the Assistant panel, its backdrop, the star banner, and both halves of the
.filter-bottom-nav-aware-panel rule, which also carried a derived 6rem.
MobileNavBar now publishes its measured height as --pulse-mobile-nav-height
and those five sites read it. Publishing happens in onMount rather than the
ref, because Solid runs refs before the node is in the document where
offsetHeight is 0; reading after mount forces layout and gets the real value
on first paint. A ResizeObserver covers content-driven changes and a resize
listener covers the xl breakpoint, where the bar swaps between its height and
0 by display alone.
The declared :root value is only a pre-measurement fallback and deliberately
under-estimates. Over-reserving is what produces the un-dimmed band; reserving
slightly less is harmless because the opaque bar paints over it.
Verified at 375x812 and 1280x800: backdrop and panel now both end at 767
against a bar top of 767, gap 0; the point 5px above the bar hit-tests to the
Assistant panel rather than page content; the filter panel resolves to 45px on
mobile; and the star banner's md:bottom-4 still wins at 16px.
Adds the regression proofs the shape guard asks for, including an
architecture test that fails if any runtime source reintroduces a literal
bar height, and records the ownership boundary in the frontend-primitives
and ai-runtime contracts.
Two counters could not answer the question they exist for.
pulse_intelligence_patrol_runs_30d was counted from the operator-facing
patrol run history, which is capped at MaxPatrolRunHistory (100). On any
install patrolling on a normal schedule that cap is reached within hours
of a thirty-day window opening, so the field reported the cap rather than
the run count, and the calls-per-run ratio derived from it was inflated by
a censored denominator. Patrol run history now carries an uncapped daily
tally beside the capped list, advanced from a persisted high-water mark so
repeated full-list saves cannot double count, pruned to 31 days, and read
in preference to the list. An install with no tally yet falls back to the
list, so the counter never regresses on upgrade.
Schema v8 split agent-side pre-mutation refusals into target-change,
prerequisite and contract categories so they would stop hiding in "other".
In production all three are zero fleet-wide and "other" still absorbs every
refusal, because the dominant reason code is the legacy preflight_refused
aggregate recorded when an agent sends no machine reason code at all.
Agents older than the typed refusal contract report every refusal that way,
so the split is starved rather than broken, and folding the two together
made those two states indistinguishable. Schema v9 counts uncoded refusals
separately.
The desktop thresholds table carries an Offline Alerts column with the
global default control and per-row toggles, but the card layout used at
narrow container widths (including zoomed desktop browsers) omitted it
entirely, so connectivity alert state was invisible and untogglable
there. Mirror both controls into the cards: an Offline alerts tile on
the Global Defaults card and an Offline row on each resource card, with
the same tri-state cycle for guests and docker containers and the same
On/Off badge for nodes, agents, PBS and docker hosts.
Refs #1738
Contract-Neutral: frontend-only: card threshold layout renders the existing offline alert controls, no API or contract delta (Refs #1738)
Contract-Neutral: Routine release-preparation cut for v6.3.0-rc.3. deployment-installability.md IS staged with the substantive active-prerelease and mobile-decision updates for this cut. The residual demand is a verification artifact, and the registry-listed proof scripts/installtests/build_release_assets_test.go plus install_docker_sh_test.go already cover this change and pass against it; they validate the packet dynamically from the repo-root VERSION rather than pinning a literal, so they have no diff to stage. Both were run green immediately before this commit and caught two real errors in the mobile-decision clauses, which are fixed here.
Contract-Neutral: Completes the already-governed mobile table readability contract on remaining Docker overview and standalone availability rows without changing the contract.
The alert card assumes an alert has a resource behind it. It reads
resourceName unguarded and guards node and resourceId with Show, so a
system alert renders only because those guards happen to be there. Nothing
stopped a later change from adding an unguarded resource field and breaking
the notification-delivery alert, which is the one alert whose whole purpose
is to be visible when nothing else can reach the operator.
Drive OverviewTab with the exact payload internal/alerts marshals for a
raised system alert, captured from the Go side rather than hand-written:
empty resourceId, empty node, resourceName "Pulse". Assert the card names
Pulse rather than inventing a resource, title-cases the hyphenated type,
carries the message and its pointer to the destinations surface, and renders
no empty node line or undefined text.
Notification delivery health was rendered by exactly one component on the
destinations config tab. A destination that stops delivering is invisible by
nature, because the failure is the channel that would have reported it, so
confining the only evidence to a tab people open once during setup means the
state persists indefinitely.
Production telemetry shows it persisting. 78 installs retry every notification
to the maxAttempts ceiling and deliver almost nothing: 155,967 attempts and 221
deliveries across a week. 50 of them have not delivered a single notification
in 30 days, and another 28 delivered normally at some point and have since
stopped, which is the worse case because nothing about the estate looks
different. The dominant terminal failure classes are authentication and
configuration, so most of these are fixable in minutes by the operator who
cannot currently tell there is anything to fix.
Extract the delivery-health load into useNotificationDeliveryHealth and render
the existing warning on the alerts overview as well as the destinations tab.
The shared hook also holds the trigger rule, so both surfaces agree on what
counts as needing attention and neither can warn before the first load
resolves.
This does not make Pulse monitor its own delivery. Every alert type is
resource-scoped and there is no system-scoped alert concept, so raising this
like any other monitored condition remains open.
Proof: useNotificationDeliveryHealth.test.tsx pins the trigger rule across
healthy, degraded, server-reported-unavailable and unreachable-endpoint cases,
plus the silent-until-loaded guard. Verified live at 1280x800 and 375x812: a
healthy queue leaves the overview clean, and a degraded payload renders the
warning above the stat cards with the dominant failure class named.
Contract-Neutral: frontend-only reuse of the existing /api/notifications/health payload on a second surface; no subsystem Canonical Files/Shared Boundaries/Extension Points delta and no API change. Proof ships as useNotificationDeliveryHealth.test.tsx; the alerts verification-artifact list enumerates existing filenames, so a new hook's own test cannot appear on it.
Alert notification delivery is gated on the alert config's activation
state: monitor.go calls SetEnabled(enabled && ActivationState ==
ActivationActive), and a new install defaults to ActivationPending. In
that state alerts keep firing and every sendAlert returns at the
disabled check with only a debug log, so nothing reaches the queue.
The destinations surface never said so. A user could configure SMTP or a
webhook, enable it, and send a test that passes, because SendTestNotification
checks only the destination's own Enabled flag and bypasses the delivery
gate entirely. The overview tab showed "Notifications paused" but the
Notifications tab, where that belief is actually formed, showed nothing.
Production telemetry shows the resulting cohort: 510 installs with an
enabled destination, alerts standing right now, and zero delivery attempts
in 7 days; 483 of them have made no attempt in 30 days. On 6.2.1 that is
357 of 1219 notification-enabled installs.
Surface the pause on the destinations tab with the consequence spelled out,
including that a passing test proves nothing while delivery is off, and
offer the activation action inline. The card stays quiet until the alert
config resolves so it cannot flash a false warning on tab open.
Proof: AlertDeliveryPausedCard.test.tsx pins the consequence copy, the
test-send caveat, the activate callback and the busy state;
alertDestinationsPresentation.test.ts pins the copy for all three paused
reasons. Verified live against a backend in pending_review at 1280x800 and
375x812: card renders above the email section, activating clears it, and
deactivating restores it.
Contract-Neutral: frontend-only disclosure of existing alert delivery activation state; no subsystem Canonical Files/Shared Boundaries/Extension Points delta, no API or payload change. Proof ships as AlertDeliveryPausedCard.test.tsx plus alertDestinationsPresentation.test.ts; the alerts verification-artifact list enumerates existing filenames, so a new component's own test cannot appear on it.
Adding a PVE/PBS source through the Connect via API path discarded the
name typed in the add dialog: the setup bootstrap only carried type and
host, and auto-registration named the new connection after the node's
self-reported hostname. A user asking for a cluster named enacon got a
connection called pve01 and could only rename it after adoption.
Carry the typed name on the one-time setup token
(SetupTokenRecord.DesiredName) via an optional name field on
/api/setup-script-url, and let the setup-token-authenticated
auto-register completion name a newly created connection from it,
falling back to the hostname-derived default when absent. Dedup and
cluster-member adoption identity stay hostname/candidate based, so the
carried name cannot fork an existing registration.
Editing a Proxmox/PBS/PMG connection raced the configured-nodes reloads
that fire during adoption (refresh_nodes, node_auto_registered): the
editor's JSX computed findEditableNode eagerly, so every reload
remounted the editor and reset the form, and the node modal state
rebuilt formData from each refreshed snapshot, wiping typed input
within seconds. Reported by a Pro customer who could only rename one
or two things per refresh window.
Thread the editing target through a non-keyed Show and an accessor so
reloads update the mounted editor instead of recreating it, and guard
the form sync with a dirty flag: once the user has typed, background
refreshes stop rebuilding the form, while a different editing target
still resyncs. Regression test proves the poll-clobber case fails
without the guard.
Contract-Neutral: behavioral bugfix: preserve unsaved node editor form input across background config reloads; no API payload, endpoint, or public contract change