Stamp LastSeen and rate-tracker samples with server receipt time instead
of the agent-reported timestamp, matching the host agent fix. A drifted
agent clock could otherwise mark a reporting Docker host or Kubernetes
cluster permanently stale and loop offline/recovery alerts.
Refs #1519
Delete generic substring pins (toContain('Button') next to
ButtonLink/ActionIconButton siblings, 'import {', 'const {') that can
never fail independently, plus the not.toContain('Pro') booby trap on
DataHandlingPanel that the word Proxmox would spuriously trip. Where
sibling coverage was incomplete, replace with a specific pin instead:
ErrorBoundary 'Button' becomes the shared-Button import line, and the
proxmoxVersion 'unknown' pin becomes the actual sentinel check.
Cover the two installer bugs fixed in c98acb9e8 so they cannot regress:
- TestRootInstallScriptSourceGuardSurvivesPipedExecution runs the source guard
through stdin the way curl ... | bash does and asserts it reaches the
installer body instead of aborting on BASH_SOURCE[0] unbound. (#1526)
- TestInstallSHAgentVersionMismatchIgnoresVPrefix drives the version check and
asserts v6.0.4 vs 6.0.4 does not warn while a real 6.0.3 vs 6.0.4 does, plus
content assertions pinning the normalization in scripts/install.sh. (#1527)
Refs #1526#1527
Two installer bugs reported after the v5 to v6 upgrades:
- The server installer (install.sh) guarded its "am I being sourced?" check
with a bare ${BASH_SOURCE[0]}. When piped to bash (curl ... | bash) there is
no source file, so under `set -u` the run aborted with "BASH_SOURCE[0]:
unbound variable" before the installer body. Default the lookup and only
early-return on a genuine source. Regression of the v5 fix in #1396. (#1526)
- The agent installer (scripts/install.sh) compared the downloaded binary's
version ("v6.0.4") against the server /api/version value ("6.0.4") verbatim,
raising a spurious mismatch warning on matching versions. Strip a leading "v"
from both sides before comparing so only a genuine difference warns. (#1527)
Refs #1526#1527
The resolved-identity match in canonical auto-register always preserved
the stored host, so the route-aware IP preference added to the agent in
022f170df could never take effect for an already-registered node: a
reinstall re-matched the node and kept the stale short-DNS host forever.
Preserve the stored host only when it is absent from the agent's ordered
candidate list (an admin-managed endpoint the agent cannot see); when the
agent itself lists the stored host as a lower-priority candidate, adopt
the newly selected host. Identity continuity (same node record, same
token) is unchanged.
Note: commit c21693489 carries this same message by mistake; it actually
contains concurrent OIDC/SAML mapping work from a parallel session that
was staged when the shared index raced. This commit is the real change.
The resolved-identity match in canonical auto-register always preserved
the stored host, so the route-aware IP preference added to the agent in
022f170df could never take effect for an already-registered node: a
reinstall re-matched the node and kept the stale short-DNS host forever.
Preserve the stored host only when it is absent from the agent's ordered
candidate list (an admin-managed endpoint the agent cannot see); when the
agent itself lists the stored host as a lower-priority candidate, adopt
the newly selected host. Identity continuity (same node record, same
token) is unchanged.
A maintenance cut of an older line (v5.1.36 after v6 GA, or a future
6.0.x patch after 6.1 ships) was allowed to move Docker/GHCR :latest and
the GitHub latest release marker onto itself, silently downgrading every
install that follows latest. Promote :MAJOR and :MAJOR.MINOR
unconditionally, but :latest and make_latest only when the tag is the
highest stable semver. workflow_dispatch gains force_latest as the
explicit rollback escape hatch.
The in-app updater and the unattended timer both target the public
rcourtman/Pulse community assets, so a Pro install that used them was
silently downgraded to community. Guard 2 (983a89326) blocked in-app
apply on the Pro edition, which stopped the downgrade but left Pro
installs with no update path except manual portal downloads. That
friction is a plausible driver of the runtime split: as of 2026-07-08
only 10 of 66 active paid licenses have any Pro-runtime install.
Root fix: the compiled Pro binary now checks and applies updates
through the license server download broker (GET /v1/downloads/pulse-pro
with the installation token and instance fingerprint). The check
compares against the broker's pinned private release instead of GitHub,
respecting the stable/rc channel guard. Apply re-resolves fresh signed
R2 URLs at apply time, verifies the archive against the same pinned
pulse-installer SSHSIG key plus the broker manifest sha256, and refuses
GitHub-shaped download URLs outright. An unactivated Pro binary still
refuses with the portal fallback. The community edition path is
unchanged.
The update banner restores in-app apply for auto-updatable Pro
deployments and keeps the portal instructions for deployments the
updater cannot drive (Docker). scripts/pulse-auto-update.sh now skips
when the installed binary reports Pulse Pro so the unattended timer can
never reinstall community over Pro.
Note: internal/updates/pro_update.go and manager_pro_update_test.go for
this change landed one commit early inside 313552deb via a parallel
session committing a shared staged index; this commit completes the
wiring they belong to.
TestUpdateDemoWorkflowUsesGovernedNetworkPath still asserted the retired
authkey: TS_AUTHKEY line; 0a9a29d63 moved update-demo-server.yml to the
OAuth client (TS_OAUTH_CLIENT_ID / TS_OAUTH_SECRET, tag:infra), so the
test now pins those lines instead.
v5 installs on 5.1.28 and older have no release-line pin, so with
auto-update enabled they silently in-place upgrade to the newest stable
GitHub release, which is now v6. Add an FAQ entry for the operator who
lands on v6 without asking, and point the rollback path at 5.1.36, the
final v5 release (was 5.1.35).
Settings > Plans told paid users on the community runtime to open the
Pro download page "with your license key", but that page rejects
license keys and demands a ppk_live_ activation key, and migrated v5
customers have never been shown one (support case: lifetime customer
pasted his v5 key, hit the prefix error, and emailed support). The
mismatch copy now names the activation key and points at
pulserelay.pro/retrieve-license, which issues one self-serve.
b0792890b added planComparisonTrialActionLabel/Note to the billing
presentation without updating the pinned copy object in
licensePresentation.test.ts (hooks do not run vitest, so it landed red).
The finding-links spec asserted Open related links into the retired
standalone routes from the pre-workbench findings list; those cross-link
affordances were removed deliberately with platform-first navigation,
and the monitor-first workbench feeds findings through a different
pipeline its stubs never reach. The run-history breakdown spec pinned
per-run type counts that the workbench simplified to plain totals; the
underlying count-separation contract (truenas_checked distinct from
agent hosts) lives in the investigation context model and is unit
covered there.
The spec entered through a retired settings route with an inert REST
stub (mention candidates come from websocket state) and the old Expand
Pulse Assistant launcher name. Re-pointed at the TrueNAS platform page
and the Ask Pulse Assistant launcher, it exposes a real regression:
Docker app-containers appear in the @-mention autocomplete but TrueNAS
app-containers no longer do, despite identical resource typing in
websocket state. The spec keeps the contract and skips via fixme until
mention targeting is restored.
The alert-links spec pinned Open related links into the retired
standalone routes, which were removed together with those routes'
cross-link affordances; its stub timestamps had also aged out of the
history window. The stubbed TrueNAS alert now uses fresh timestamps, the
Resource action is scoped to its own row (live mock alerts share the
page), and the contract asserts the surviving canonical handoff: the
resource incidents panel scoped to the alerting resource.
The disk-history contract lived on the retired /storage route with REST
stubs the platform pages never read. It now drives the TrueNAS storage
section against the mock dataset: the SMART-failing sdc disk opens the
resource drawer, and the History tab must request the disk series from
the metrics store under a canonical key (serial when available, the
disk:<node>:<device> composite otherwise - the store serves both, which
is what protects serial-less disks).
GrantClaims and Claims gain a MaxUsers seat limit mirroring MaxGuests:
the grant JWT claim is copied at the activation bridge and surfaced
through EffectiveLimits()["max_users"], which the already-shipped
user-limit enforcement (MaxUsersLimitFromLicense) reads unchanged.
Inert by default: no plan sets max_users yet, so absent claim = 0 =
unlimited for every existing license and grant. The self-hosted
commercial volume scrub strips only max_monitored_systems and
max_guests; tests now pin that max_users survives it from both the
named field and explicit limits, plus the end-to-end proof that a grant
carrying max_users=3 enforces 3 and a grant without it stays unlimited.
Contract shape recorded in cloud-paid Extension Point 26; the grant
wire contract list gains max_users against the relay-server reference
(pulse-pro 95a18bd).
The thresholds surface replaced the neutral infrastructure/systems/
containers groupings with per-platform scopes, so the TrueNAS routing
contract inverted by design: TrueNAS systems, pools, datasets, and disks
now live under their own scope button. The spec asserts that scope's
sections against the mock dataset instead of the retired neutral pages.