The internal/api race suite now routinely exceeds the old 20-minute package timeout on hosted runners while passing. Set a governed 30-minute package timeout and 40-minute release job ceiling, pin the relationship with contract tests, and refresh the rc.4 packet with the fixes landed since preparation.
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
A hard-mounted network filesystem with an unreachable server blocks
statfs in an uninterruptible kernel wait. The collector issued that
syscall inline for every mount, so one dead NFS mount silently froze
the whole reporting cycle and stretched agent shutdown into the kernel
retry window, and it paid that price for mounts the fstype filter was
going to discard anyway.
Decide type- and mountpoint-based skips before the usage syscall, so
network filesystems are never probed unless explicitly included, and
bound every remaining usage call with a timeout that leaves at most one
in-flight probe per mountpoint. A mount whose call never returned is
skipped on later cycles and re-included when the stalled call answers.
Refs discussion #1747
Contract-Neutral: Discussion #1747: behavioral bugfix in host disk collection; no payload or API shape change, filtered mounts were never reported
The installer staged the download in /tmp and installed the runtime
binary to /usr/local/bin, both on the small RAM-backed QTS/QuTS hero
root, and the boot wrapper copied 34MiB back onto that root at every
boot. Roots without ~50MiB of headroom could not install at all, and
setting TMPDIR only moved the staging half of the requirement.
QNAP's own QPKG packages execute from the data volume, so do the same:
relocate the install dir to the data volume's state dir before the
preflight and download, default TMPDIR there too, skip the boot-time
self-copy when the stored and runtime binaries are one file, and remove
a pre-relocation runtime copy from /usr/local/bin to give that space
back. Split layouts with an operator-supplied state dir keep the copy
semantics. The rendered wrapper is exercised in both layouts by the
installer tests.
Refs #1617
Contract-Neutral: Refs #1617: QNAP installer layout fix with its deployment-installability contract clause staged in this commit; residual proof policies for unrelated boundaries do not apply to this shell-only change
Since the operational-trust records shipped in v6.2.0, every new firing
of a previously resolved alert folded into that alert's first history
row: setActiveAlertNoLock stamped the new occurrence's open record onto
the previous occurrence's resolved row, and the history dedup then
treated any two open records with the same identity as one incident
regardless of how far apart they were. Alert history therefore froze at
the upgrade date while notifications kept flowing, which is exactly how
users reported it.
Resolved rows now keep their final record unless the update belongs to
the same occurrence, and two open records no longer merge on identity
alone. The observation-gap window still coalesces genuine flapping, and
the five-minute refire continuity path is unchanged.
Refs #1497
Contract-Neutral: Refs #1497: behavioral bugfix in alert history occurrence dedup; no API shape change, history row schema unchanged, no subsystem contract names sameHistoryIncident
The default attempt budget was three, and with the 1s/2s/4s backoff a
destination that was unreachable for about ten seconds had its
notifications dead-lettered permanently. A webhook receiver rebooting
alongside the infrastructure it monitors is routine, not terminal.
Eight attempts under the same doubling schedule span roughly three
minutes before dead-lettering. Dead-letter semantics are unchanged.
Refs #1721
Contract-Neutral: Refs #1721: raises the default delivery attempt budget only; dead-letter semantics, retention, and outcome vocabulary unchanged, notifications.md pins no attempt count
normalizeOverrides ran every override through NormalizePoweredOffSeverity,
which maps an unset severity to an explicit warning. Any guest with any
per-guest override (a disk tweak, a note) therefore had its powered-off
alerts silently downgraded from a global critical default to warning, and
the stamped value also round-tripped back to the UI as if the user had
chosen it. Leave unset severities empty so the merge keeps following the
global default, and normalize only values the user actually set.
Refs #1738
Contract-Neutral: Refs #1738: behavioral bugfix in alerts override normalization; poweredOffSeverity stays an optional field, no subsystem contract names it, no API shape change
The guest Docker socket probe hung minipc hard enough to need a power
cycle (2026-08-20): ~100 orphaned pct exec children, load 133, sshd and
pveproxy starved. Three bugs chained, each fixed here:
1. Dispatcher re-issued a probe while the previous one was still
executing. The poll cycle's enrichment context had expired, so
ExecuteCommand dispatched, returned the context error 50ms later,
and the next 3s cycle sent the identical command again — unbounded
concurrency against a host that was slow to begin with. The
monitoring dispatcher now takes a per-guest in-flight claim before
dispatching probe or inventory commands (completed probes release
it; abandoned ones hold it for a 2-minute window), and both dispatch
paths bail out under a dead context.
2. The host agent never got the July process-leak fix: 45480a5cc
landed only on pulse/v6-release, so main-line agents killed just the
direct shell on timeout, orphaning pct exec → lxc-attach children
and blocking Wait on their inherited pipes (10s timeouts reported as
300s+ durations). Port it: run each command in its own process
group, SIGKILL the group on cancel, bound Wait with WaitDelay, and
treat ErrWaitDelay after a clean exit as success.
3. Server-side abandonment never reached the agent. ExecuteCommand and
ReadFile now refuse to dispatch under an already-expired context,
and send a best-effort cancel_command when they stop waiting; the
agent cancels the in-flight execution (killing its process group)
and reports "command canceled". Older agents ignore the unknown
message type.
Also add a per-node circuit breaker: three consecutive command failures
on one node suspend all Docker probe/inventory dispatch to it on the
existing 1m→30m backoff schedule, so a host-level stall (NFS flapping)
stops the probing entirely instead of failing guest by guest.
Regression tests simulate the storm without hardware: a never-returning
executor is not re-issued across poll cycles, an expired context
dispatches nothing and records no failure, abandoned probes hold their
claim, the breaker blocks new guests on a failing node, and the agent
kills the whole process group on timeout and on server-issued cancel.
Contract-Neutral: monitor.go delta is three private struct fields holding Docker probe dispatch state; host-agent deletion/re-enrollment lifecycle untouched — contracts and all other proofs are staged
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.
Live proof on a real systemd host (Debian 12 container, full
install/report/update/uninstall cycle) caught what no unit test did:
NoNewPrivileges=true blocks sudo outright, so the --grant-smart and
--grant-pct helpers failed inside the service and SMART/pct silently
disappeared while the install reported success. A unit with an active
grant now sets NoNewPrivileges=false, keeping the rest of the hardening;
a grantless least-privilege install keeps NNP enabled. Docs and contracts
state the trade-off: each grant is a scoped, auditable widening.
Also proven in the same live cycle: the flag-conflict refusals, user and
sudoers provisioning, wrapper execution as the service user, the healthy
privilege report ({runningAsRoot:false, serviceUser:pulse-agent,
smartctlHelper:true}), --update preserving the profile without repeated
flags, and uninstall removing the sudoers file and helpers.
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.
Evaluators keep rejecting Pulse over the agent's root default without
discovering that Proxmox monitoring needs no agent at all. The README now
answers 'do you need an agent?' where agent installation is introduced:
API-only monitoring first, agents only for data the platform APIs cannot
provide, and the security model (commands off by default, localhost
listener, hardened units, the least-privilege profile) linked from the
same place.
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
LoadAIFindings and LoadAIChatSessions swallow parse failures by returning
empty data with a nil error, and an undecryptable file lands in the same
branch because a failed Decrypt falls back to parsing the ciphertext as
JSON. Every read-modify-write saver (SaveAIFindingsWithSuppression,
SaveAIChatSession, DeleteAIChatSession, CleanupOldAIChatSessions) then
sees "empty, no error" and rewrites the file, permanently destroying
whatever it still held — user-authored suppression rules and chat
conversations. 9038afc68 fixed the read-error variant of this clobber;
this is the parse/decrypt variant that bypassed that abort because the
loader hid the failure.
Policy is preserve-for-recovery: the loader moves the unreadable file
aside to <file>.corrupt-<timestamp> and logs a warning before returning
empty data, keeping the self-healing behavior without the silent data
destruction. If the move itself fails, the load now fails, so the savers
abort instead of rewriting. recordActivityHistoryLocked keeps its
reset-on-parse-error behavior — those files are content-free telemetry
markers.
Tests cover corrupt and wrong-key-encrypted findings/session files being
preserved across the previously clobbering saves, and the move-failure
path aborting every saver while leaving the corrupt file intact.
The dormant self-hosted business tier opens for public checkout under the
dials the owner approved on 2026-08-20: \$399/year annual-only, unlimited
seats, 365-day history, next-business-day support target, Pro-identical
features per cloud-paid Extension Point 26. Newly issued Pro
subscriptions carry a 3-seat max_users limit on new Stripe price ids;
previously issued licenses keep their unlimited posture because their
plan entries are untouched. The 2026-08-08 opt-in presentation posture is
unchanged: Business appears only through the public pricing-model payload
and checkout, and no proactive in-product surface returns.
Adds the decision record, resolves the decision in status.json, lifts the
EP26 dormancy sentence, and extends the subsystem_lookup decision-id pin.
License-server and landing changes land separately in pulse-pro.
SaveAIFindingsWithSuppression and SavePatrolRunHistory read the existing
file to carry data forward (suppression rules, the daily run tally) and
treated a failed read as nothing to preserve, rewriting the file without
it. Same clobber mechanic as the system.json reset fixed in cf5b86e62.
Both loaders return empty data with no error for a missing file, so an
error at these sites is a real read failure.
Suppression rules are user-authored config, so that save now aborts and
returns the read error. The run tally is telemetry, so that save
proceeds but logs a warning that the tally restarts. Tests cover the
abort leaving the file intact, explicit rules saving despite a failing
read, and the tally warning plus restart.
A LoadSystemSettings error in configureMonitorDependencies silently
skipped the tenant-monitor inherit block, leaving the monitor on
deny-all-private webhook CIDRs with nothing in the logs to explain why
private webhook targets fail SSRF validation. reloadSystemSettings
fails closed on the same error (embedding off) deliberately, but was
equally silent, so a persistent read failure looked identical to
embedding being switched off on purpose.
Both paths now log a warning with the error, mirroring the pattern from
cf5b86e62 in internal/config. A missing system.json (fresh install)
still stays quiet. Tests cover the warning on read failure, the quiet
fresh-install path, and that the reload still fails closed.
A boot-time LoadSystemSettings error fell into the missing-file branch:
no log line, every persisted system setting reverted to defaults for
the run, and a default system.json was written over the file on disk.
Observed once in practice (2026-08-20): a restart came up with a
persisted toggle unset while system.json still held the correct value.
Distinguish the error from the genuinely-missing file: retry the read
once (transient failures at boot were the observed shape), then warn
with the error and file path and run on defaults without touching
system.json. Tests cover the warning, the untouched file, and the
retry-once behavior.
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).
Mock mode never exercised the backup-running states from "Stop showing
an in-flight backup as a completed one": no guest carried
BackupInProgress, no PBS listing showed an in-flight snapshot, and no
vzdump task was ever running, so the blue Running badge and the Running
artifact chips had no local reproduction.
A post-scenario fixture now flags roughly one guest in twenty-five per
last-backup age bucket - never, fresh, stale, overdue - so the Running
badge appears alongside every badge story, and gives each flagged guest
the evidence a live run actually produces: an in-flight PBS snapshot
shaped the way the PBS API lists one still being written (no size, no
verification, only the guest config blob in files) and a running vzdump
task with no end time. Running after applyDemoBackupScenario keeps the
per-tick reapplication idempotent, because the scenario's curated sweep
drops the previous pass's artifacts before the fixture re-adds them.
The first-cycle showcase guests are excluded: their exact rows are
pinned by the demo posture tests (a failed run three hours ago, a guest
with no recovery evidence at all), and a backup running right now would
mask exactly the states those rows exist to demonstrate. Later profile
cycles repeat the same stories under suffixed names, so the fixture
draws its running guests from them.
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 first cut of the fixture ran inside buildFixtureState, but every
fixture graph build and metric tick then applies the demo scenario
layer, which renames guests, aliases cluster instances, forces guest
states, and reprofiles every Docker host. The nested hosts got swept
into the Docker fleet profile cycle: renamed away from their parent
guests, handed native engine inventory the production pct exec path
never reports, and one could be forced offline as the demo's
disconnected host.
The fixture now runs from applyDemoScenarioGraph after the workload
and Docker scenarios, so nested hosts bind to the guests' final names,
instances, and states, and it is idempotent across ticks: an existing
nested host re-syncs from its guest instead of duplicating, one whose
guest stops running goes offline like a real probe target, and the
sync strips every native-agent surface each pass. The Docker scenario
now skips proxmox-lxc-docker: hosts entirely, with the forced-offline
index counted over native hosts only.
The Proxmox page's nested-container row cue, the drawer's nested Docker
card, and the proxmox-pve platform scoping of nested workloads had no
local reproduction at all: mock mode never generated a Docker host with
the proxmox-lxc-docker: identity prefix, so the surface could only be
seen against a live estate with node agents, exec-scoped tokens, and
the server-side inventory opt-in. That is how it shipped invisible.
The fixture now marks the first two running LXC guests as probed
Docker hosts and appends a nested Docker host for each, following the
production proxmoxGuestDockerAgentID convention of
proxmox-lxc-docker:<instance>:<node>:<vmid>. One guest nests a single
container and one nests several, so the row cue renders both its
singular and plural counts. The nested hosts mirror the shape of the
real pct exec inventory: guest-derived sizing and usage, one rootfs
disk, and no native engine inventory, host I/O rates, temperature,
machine ID, or Swarm membership.
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.
lxc-attach into an unprivileged guest writes /proc/<pid>/uid_map, which
needs CAP_SETUID in the parent user namespace. NoNewPrivileges drops
CAP_SETUID from the effective set and also stops lxc-attach falling back
to the setuid newuidmap/newgidmap helpers, so the socket probe dies with
"write_id_mapping: 61 Operation not permitted".
install.sh already relaxed NoNewPrivileges for this, but only when the
agent was installed with --enable-commands. Command execution is also
togglable from the server afterwards: applyRemoteConfig starts the
command client without rewriting the unit. An agent installed without
the flag and switched on later therefore ends up able to run commands
and unable to attach to unprivileged guests, so Docker inside every
unprivileged LXC disappears from the Proxmox page. The probe failure is
logged at debug level and retried on every poll, so the surface looks
empty rather than broken while the agent re-probes the whole guest list.
Grant CAP_SETUID/CAP_SETGID to any PVE agent rather than gating on the
install-time flag, so the later toggle lands on a unit that can attach.
Ambient capabilities restore exactly the privilege lxc-attach needs and
leave the rest of the sandbox intact; the existing install-time
relaxation is unchanged.
Verified on a live PVE node. With the hardened unit the probe succeeded
only on the three privileged guests and failed on every unprivileged
one. After the ambient grant CapEff regained CAP_SETUID and both
unprivileged Docker guests were discovered, taking that node from one
Docker LXC to three.
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.
An agent whose token authenticated seconds ago cannot be in a
credential outage, yet the fleet doctor flagged it critical with
"missing or revoked" whenever the token id missed the server's token
registry view, and the offered repair-authentication loop cannot fix a
stale server-side registry view. When the subject's own row shows the
judged token authenticating within the freshness window, emit a
warning-severity registry-stale verdict naming the id and the last
authentication instead. Genuinely stale rows keep the critical verdict.
Refs #1730
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.
Running any release-control script leaves __pycache__ directories in the
tree. scripts/release_control/* already ignores the top-level one as a
side effect of its blanket wildcard, but scripts/release_control/internal/
is deliberately re-included further down so its 31 tracked scripts stay
visible, and that re-inclusion left internal/__pycache__ uncovered. It
showed as untracked in every agent's git status and was one stray
git add -A away from committing bytecode.
Cover it repo-wide rather than patching the one path, so future Python
directories stay clean too. Verified that the rule shadows no tracked
file, the set of tracked-but-ignored paths is unchanged at 27 either
side of the change.
ec8b88fe2 registered the alert-form docker identities as Patrol scope
aliases for hosts and containers, but Swarm service alerts publish a
third form, docker:<host>/service/<serviceID>, which matches neither
the host alias docker:<host> nor the container alias
docker:<host>/<containerID>. Have Patrol investigate on a docker
service alert still returned patrol_scope_unresolved, and the
automatic alert-fired patrol path sent the same identity, so scoped
runs it triggered for service alerts failed resolution the same way.
Mirror the service resource IDs onto the owning docker host record.
Patrol has no service-level analysis, and a service's tasks run as
containers on the host, so the host is the smallest unit Patrol can
investigate for a service alert. Export the alerts helper for the same
reason ec8b88fe2 exported DockerResourceID, so the identity format
cannot drift between the two subsystems.
Hosts without an ID are skipped. The host-less fallback form
docker-service:<name> is not host qualified and would alias unrelated
records together across hosts, which is the same anti-aliasing rule
dockerAlertScopeAlias already applies.
Covered by tests asserting the alias equals what the alerts subsystem
emits for the same input, that the host-less guard registers nothing,
and that a service without an ID follows the canonical name-derived
form.
Refs discussion #1699
Contract-Neutral: behavioral bug fix refs discussion 1699, patrol scope resolution could not resolve docker service alert resource IDs, no public contract delta
TestRecoveryPointsEndpointReturnsMockData sampled the unfiltered first page
of /api/recovery/points and expected to find a kubernetes point in it. That
held only while the mock estate was small.
Points come back newest-completed-first and a page is hard-capped at 500. The
rescaled Proxmox demo estate now generates 2498 points, 939 of them newer than
the newest kubernetes point, so kubernetes starts at index 939 and never
reaches page one. The 36 kubernetes points are still generated and still
served, and the endpoint, the store ordering and the fixture shape are all
correct: a platform that snapshots every few days legitimately falls behind a
platform that backs up hourly. TrueNAS was already at index 254 and would have
broken next.
Ask the endpoint per platform instead, which is how the API is designed to
answer the question and how the UI asks it. The assertion no longer tracks
estate size, and it additionally covers the platform filter.
Contract-Neutral: test-only change, no behavior or contract delta.
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 Proxmox node sweep removes any non-preserved alert whose Node is
empty, and system alerts have no node, so every sweep silently deleted
the notification-delivery alert. Its five-minute evaluation then
re-raised it as a brand-new alert, firing a fresh notification each
cycle with no recovery in between, which reads as an alert appearing,
vanishing without a recovery, and paging again minutes later. System
alerts are now preserved outside node cleanup.
Raises also carried the delivery counts inside the message, and a
message change re-notifies, so each newly retained failure re-paged
even while the condition was unchanged. System alerts now take an
optional fingerprint: while level and fingerprint hold, a re-raise
refreshes the message and metadata silently. The delivery alert
fingerprints on status and failure classes, keeping its counter text
current without paging on drift.
Refs #1721
Contract-Neutral: behavioral fix: delivery-health system alert survived node sweep and stops re-paging on count drift, no public contract delta (Refs #1721)