Commit graph

3360 commits

Author SHA1 Message Date
rcourtman
f850099134 Adopt the agent's preferred host when re-registering a matched node
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.
2026-07-09 09:56:04 +01:00
rcourtman
c21693489a Adopt the agent's preferred host when re-registering a matched node
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.
2026-07-09 09:52:59 +01:00
rcourtman
93bccaff18 Update the Pro binary in-app from the license server download broker
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Canonical Governance / governance (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/4) (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
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.
2026-07-08 23:39:37 +01:00
rcourtman
313552debc Pin the OAuth Tailscale path in the update-demo workflow test
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.
2026-07-08 23:23:20 +01:00
rcourtman
fca5975c38 Add the dormant Business tier to the licensing core
TierBusiness: Pro's feature set, 365-day history retention, uncapped
core monitoring like every self-hosted tier, Business display name, and
a slot in the min-tier ordering. Differentiation is by max_users limit,
retention, and support rather than features (multi-user is already a Pro
capability via RBAC; see specs/pricing-segmentation.md binder findings).
Dormant until the license server issues business plan versions; no
checkout, pricing, or UI surface references it yet. Contract shape
recorded as cloud-paid Extension Point 26 and pinned by
TestBusinessTierContractShape.
2026-07-08 16:02:56 +01:00
rcourtman
b0792890b0 Add the License-panel Pro trial front door (pull-only)
Completes the approved trial flow end to end. The License panel's plan
comparison gains a 'Start 14-day free Pro trial' action (shown only when
unlicensed, trial-eligible, and never-trialed) that rides the existing
purchase-start handoff with trial=1; the handler already forwards
non-reserved params. Pulse Account portal reads the trial arrival flag,
relabels the pro monthly checkout button, shows the card-required note,
and sends trial:true to the checkout session endpoint (server support
deployed in pulse-pro 4c3a28c). Organic portal and public pricing
arrivals see no trial anywhere, per the no-upsell doctrine; retired
local trial-acquisition routes remain 404. Portal dist rebuilt.
2026-07-08 15:29:54 +01:00
rcourtman
07fb7dbbac Serve shipped markdown docs as text instead of a forced download
The embedded frontend file server had no content type for .md, so every
in-app "Full details" / "Terms of Service" / security-guide link to
/docs/*.md answered application/octet-stream and the browser downloaded
the file instead of showing it. Shipped docs now serve as
text/plain; charset=utf-8 and open readable in the tab the app targets.
2026-07-08 14:36:55 +01:00
rcourtman
983a89326f Block in-app self-update on the Pro binary to prevent silent downgrade
The in-app updater and install.sh both fetch the public community build from
github.com/rcourtman/Pulse. On non-Docker Pro installs (systemd, proxmoxve,
source) the "Apply Update" button was live, so applying an update replaced the
separately compiled Pro binary with community and silently stripped Audit,
RBAC, Reporting, and SSO from a paying customer. Docker was already blocked;
nothing else was.

Add a dependency-free pkg/edition marker (defaults to community) that the Pro
binary flips via enterpriseruntime.Initialize, mirroring the existing
coreaudit.SetLogger / server.SetBusinessHooks registration seam. ApplyUpdate
now refuses when the edition is Pro, pointing at the Private Release Access
portal (https://pulserelay.pro/download.html) and the install.sh --archive
path. The gate keys off the compiled binary, not license state: a community
binary with an active license still self-updates as before.

The update banner hides the in-app apply affordance and shows portal
instructions for the Pro runtime, keyed off the existing runtime-identity
signal (runtime.build) rather than a new payload field, so nothing extra is
plumbed through the version API and the frontend reuses the canonical
"which binary am I" contract. The backend gate is the hard guarantee; the
banner is the UX layer.

Guard 2 of the Pro download/update experience spec.
2026-07-08 10:02:04 +01:00
rcourtman
9923e4b04f Pace mock churn to homelab rates and stop phantom demo change spam
The public demo wrote ~110K resource_changes rows/day (restart 60K/day,
state_transition 45K/day), making the Changes timeline unreadable and
keeping unified_resources.db churning. Four generator-level engines,
all verified with before/after soaks against scratch mock backends:

- Per-tick flap probabilities ran 43,200x/day on the 2s update loop
  (docker restart p=0.01/tick alone is ~430 restarts/day/container).
  Churn rates are now expressed as events per day per entity and
  converted per tick against the configured interval, tuned to a few
  fleet-wide events per day with dwell times long enough to see.

- The pod scheduling reconciler fought the per-tick scenario re-pin
  and fabricated a fresh random StartTime on every recovery; derived
  uptime moved backwards, which change emission records as a restart.
  The reconciler is now idempotent: stable per-pod park/reschedule
  choices, StartTime never regenerated, and only pods it parked
  itself (NodeNotReady/ClusterOffline/NodeLost) get recovered, so
  curated Pending and ImagePullBackOff stories stay put.

- Swarm cluster objects were fabricated per host under one shared
  cluster key, so registry dedupe alternated between the divergent
  candidates every poll (service renames, status flips, node
  re-parenting). One leader manager now reports services, tasks,
  secrets, configs and the node inventory, like a real control plane.

- Demo docker host profiles cycled 2 hostnames across 4 online hosts,
  collapsing canonical identities, and scripted-offline hosts had
  their sighting refreshed right at the 2 minute staleness threshold,
  sawtoothing them online/offline. Four distinct host profiles now
  exist and offline hosts keep a stably stale sighting.

Before/after soak with a live client: pre-fix ~110-160 rows/min
sustained (restart ~45/min, matching the droplet's 60K/day); post-fix
zero rows/min at steady state with the curated degraded stories
(CrashLoop payments-worker, ImagePullBackOff, offline hosts) intact.
2026-07-08 09:49:03 +01:00
rcourtman
ef2569b1fc Pick cluster-scoped swarm dedupe winners deterministically
In a multi-manager swarm every manager reports the same cluster-scoped
objects (services, nodes, secrets, configs), and the registry dedupe
picked the candidate whose host had the freshest LastSeen. That ordering
flips between polls, so the winner alternated: swarm node resources
re-parented to whichever manager won (ParentID tracks the reporting
host), and services flipped name/status/updateStatus whenever the
managers' views differed slightly. Every rebuild then wrote phantom
relationship_change / state_transition / config_update rows into
resource_changes, the same class of churn as the registry-rebuild spam
fixed in 74131e56e. Mock mode had the same symptom and was fixed by
pinning a single reporting leader, but real deployments still hit this.

Select winners deterministically instead: richer candidate fields first
(unchanged intent), then managers with an available control plane, and
freshness only when the gap exceeds the docker stale threshold, i.e.
the losing manager has genuinely gone quiet; otherwise the lowest host
ID wins as a stable tiebreak. Equal candidates can no longer alternate.

Regression test rebuilds the registry across polls with two managers,
jittered LastSeen ordering, flipped snapshot host order, and slightly
divergent service views, asserting no change emission; it fails against
the old LastSeen-ordering rule with the exact swarm-node re-parenting
seen in production.
2026-07-08 09:35:56 +01:00
rcourtman
a54e67cb0f Point agent 401 recovery copy at the Pulse UI, not a wrong breadcrumb
Refs #1515

The install command is generated under the Settings infrastructure
installer, not a "Settings > Agents" tab. Correct the agent log, installer
warning, and Machines tooltip to say "the Pulse UI" so the recovery step is
accurate.
2026-07-08 08:40:29 +01:00
rcourtman
c7dcd90b83 Surface agent auth failures and staleness instead of a silent 401 loop
Refs #1515

When an upgraded or restored Pulse server no longer recognises an agent's
API token, the report endpoint returns 401. The agent buffered and retried
that report forever with only a generic warning, and the server kept the
node green at its last known agent version because a Proxmox node stays
online via the PVE API poll even after its agent dies.

- Agent: special-case 401 on /api/agents/agent/report. Drop the report
  instead of buffering it and log a throttled, actionable error pointing the
  operator at the install command to mint a fresh token.
- Installer: verify_agent_server_registration now tells a rejected token
  (401/403) apart from "agent has not reported yet" and prints the recovery
  steps at install time instead of a vague soft warning.
- Status layer: resourceFromHost flags a stale agent (its host marked
  offline by the staleness evaluator) and carries the agent's own last
  report time. Coalescing keeps a node online via the PVE source but the
  dead agent stays flagged, so its version is no longer presented as current.
  The Machines table renders such versions as "(stale)".
2026-07-08 08:35:46 +01:00
rcourtman
a37c38f0ec chore(cloudcp/portal): patch dev-dependency advisories
Bump vitest 3.2.4 to 3.2.7 (clears critical GHSA-5xrq-8626-4rwp) and pin
patched transitives via overrides: vite ^7.3.5, esbuild 0.28.1 ($esbuild),
form-data ^4.0.6, postcss ^8.5.10, ws ^8.21.0. Clears 9 Dependabot alerts
(1 critical, 4 high, 3 medium, 1 low) plus a pre-existing ws high-severity
that npm audit surfaced. npm audit now reports 0 vulnerabilities; build,
test, and typecheck green. Refreshes dist build_manifest source_hash for the
new package.json.
2026-07-08 08:25:45 +01:00
rcourtman
590f8c34db Serve legacy 3-segment OIDC SSO login and callback paths
An OIDC provider upgraded from v5 keeps its v5 redirect URL
(/api/oidc/callback, the DefaultOIDCCallbackPath), so the IdP redirects
the browser back to the 3-segment legacy path carrying only code and
state, with no session cookie and no API token yet. The global auth
middleware only marked the 4-segment per-provider path public, so in
API-token-only mode (AuthUser=="" && AuthPass=="" && HasAPITokens())
the callback was rejected with "API token required via Authorization
header or X-API-Token header", and the route dispatcher 404ed the same
path.

The handler layer (extractOIDCProviderID) already maps the legacy
/api/oidc/login and /api/oidc/callback paths to the migrated legacy
provider, so only the public-path allowlist and the route dispatcher
needed to recognise the 3-segment form. Adds a regression test covering
both legacy paths in API-token-only mode, the exact configuration that
emitted the error.

Refs #1533
2026-07-08 08:15:03 +01:00
rcourtman
74131e56e3 Fix no-op relationship_change spam from registry rebuilds
Every state update rebuilds the resource registry from scratch, which
reconstructs all relationship edges with fresh ObservedAt/LastSeenAt
stamps and fresh metadata maps. recordRegistryChanges compared the old
and new slices with reflect.DeepEqual, so every relationship-bearing
resource emitted a relationship_change row on every rebuild cycle even
when nothing changed. On the public demo this wrote roughly 450k rows
per day (1.2M of 1.6M rows were literal from==to no-ops), grew
unified_resources.db to 1.6GB in three days, starved the store's single
connection until retention pruning failed with SQLITE_BUSY, and drove
the droplet into the swap-thrash outage on 2026-07-08. Same mechanism
as issue #1496.

Compare relationship sets by edge identity instead: canonical source,
canonical target, type, and active state, order-insensitive. Volatile
provenance fields no longer count as change.

Also cap resource_changes at 200k rows during retention pruning so a
pathological writer can never grow the table unbounded inside the
30-day retention window, and record both invariants in the
unified-resources subsystem contract.
2026-07-08 08:10:15 +01:00
rcourtman
b6fd06c63d Let explicit disk temperature overrides beat per-type defaults
DiskTempByType unconditionally replaced the resolved host threshold, so a
host-level (or inherited linked-resource) diskTemperature override never
applied to nvme/sas/sata disks. Explicit overrides now win; the per-type
map still beats the global agent default.

Refs #1540
2026-07-07 23:15:59 +01:00
rcourtman
23a930e849 fix(discovery): bring hosts into the fingerprint model so they auto-discover (#1479)
Hosts (PVE nodes and host agents) were entirely outside the discovery
fingerprint model. Two consequences, one root:

1. collectFingerprints only covered docker/lxc/vm/k8s, so a host never
   appeared in GetChangedResources and automatic discovery never ran for
   it. Only the per-resource manual trigger worked, which is exactly the
   v6.0.5-rc.3 behaviour reported in #1479.

2. cleanupOrphanedData built its keep-set from the same four types, so
   CleanupOrphanedDiscoveries swept every agent:* discovery as an orphan
   on the next fingerprint cycle (default 5 minutes). Manually discovered
   hosts silently reverted to undiscovered.

Fix: add GenerateHostFingerprint (identity, OS, kernel, arch, tags;
status excluded so online/offline flapping cannot trigger rediscovery),
fingerprint snap.Hosts under the canonical agent:<id>:<id> key, and keep
host/node discovery keys (canonical, hostname-alias, and legacy host:
prefix forms) out of orphan cleanup. Also purge the store's in-memory
cache when an orphaned discovery file is removed, so deletions are not
masked for the cache TTL.
2026-07-07 22:55:04 +01:00
rcourtman
292baf308b Fix PBS backup discovery regression from bounded polling
The RC3 memory bound for PBS backup polling summarized any group with
more than 8 snapshots into a single synthesized entry built from group
metadata. A synthesized entry has no verification, size, file, or
per-snapshot time data, so most real deployments saw every backup as
Unverified with no size, PBS files not listed, and a backup timeline
collapsed onto the latest backup day.

Keep the issue #1524 memory bounds but derive them from real data:
always fetch snapshots for stale groups, retain the newest bounded set
per group (limit raised from 8 to 100 to cover real keep policies), and
keep the newest-first global live-state cap. Remove the synthesized
group placeholder path entirely and update the monitoring subsystem
contract and tests to pin real-snapshot bounding.

Fixes #1541
Refs #1524
2026-07-07 22:48:34 +01:00
rcourtman
c6fdd9058c Allow clearing SSO provider restriction fields from Settings
Empty allowedGroups, allowedDomains, allowedEmails, groupsClaim and
groupRoleMappings in a provider PUT were silently restored from the
existing config, so an admin could never clear them. The guards
shielded against lossy round-trips that no longer exist: the detail
GET and the flat list response both carry these fields, so an empty
value is an intentional clear. Nested OIDC/SAML config and the client
secret stay preserved since toggle payloads omit them and secrets are
never echoed in reads.

Also expose groupsClaim and groupRoleMappings in the shared extensions
list response so the enterprise list matches the core one, and make
the Settings enable/disable toggle send only writable fields; the old
list-item spread included computed response fields that the enterprise
strict PUT decoder rejects.
2026-07-07 22:04:46 +01:00
rcourtman
8355335e08 Clarify v5 migration recovery states 2026-07-07 21:46:07 +01:00
rcourtman
4043c466f6 Extend MSP install proof for portal rollups 2026-07-07 20:49:55 +01:00
rcourtman
4382919447 Add MSP report scheduling and alert rollup 2026-07-07 20:37:18 +01:00
rcourtman
eb99d7a6b3 Fix SSO session display labels
Refs #1535
2026-07-07 16:35:07 +01:00
rcourtman
57139c65c5 Fix mobile onboarding URL handoff sanitization
Some checks are pending
Canonical Governance / governance (push) Waiting to run
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Core E2E Tests / Playwright Core E2E (push) Waiting to run
2026-07-07 15:12:59 +01:00
rcourtman
022f170dff Prefer route-aware Proxmox host URLs 2026-07-07 10:37:10 +01:00
rcourtman
3dd2ecd198 Add Proxmox token smoke diagnostics
Refs #1476
2026-07-07 10:10:16 +01:00
rcourtman
1c8a9346ef Fix legacy OIDC SSO discovery and CSP nonce
Refs #1533
2026-07-07 09:54:14 +01:00
rcourtman
a8ce7a2919 Prove bounded PBS backup polling
Refs #1524
2026-07-07 09:52:10 +01:00
rcourtman
d52231855d Fix Proxmox token preservation regressions
Refs #1470, #1476, #1511
2026-07-07 09:49:26 +01:00
rcourtman
58ece3c1b8 Fix physical disk SMART/Proxmox merge identity
Refs #1516

Refs #1483

Refs #1471
2026-07-07 09:46:51 +01:00
rcourtman
8eed26d653 Bound PBS backup polling memory
Refs #1524
2026-07-07 09:42:54 +01:00
rcourtman
4b669ad9b5 Fix SAT SMART temperature collection
Refs #1471
2026-07-06 23:42:00 +01:00
rcourtman
af7f7c5d11 Fix Gemini Patrol tool-call readiness
Refs #1530
2026-07-06 23:01:16 +01:00
rcourtman
7ddc2fcb47 Fix Docker app-container percent normalization
Refs #1525
2026-07-06 16:11:36 +01:00
rcourtman
7edfa1bb86 Fix Docker container URL metadata identity
Refs #1490
2026-07-06 15:48:26 +01:00
rcourtman
92951532e3 Fix host agent clock-skew liveness
Refs #1519
2026-07-06 15:07:12 +01:00
rcourtman
6f582671b0 Fix Proxmox live chart CPU fallback
Refs #1525
2026-07-06 15:05:18 +01:00
rcourtman
21b15e6755 Fix Proxmox read-state CPU normalization
Refs #1525
2026-07-06 14:33:41 +01:00
rcourtman
e7334e66e4 Allow local DNS agent HTTP URLs
Refs #1522
2026-07-06 13:53:19 +01:00
rcourtman
caa9b41834 Fix OIDC provider detail persistence
Refs #1521
2026-07-06 11:18:18 +01:00
rcourtman
fce4317176 Fix PBS backup memory retention
Refs #1524
2026-07-06 10:32:38 +01:00
rcourtman
c9c5f34eb4 Fix host metric percent normalization
Apply token-gated command policy when deriving connection fleet state.
2026-07-06 10:08:36 +01:00
rcourtman
04a9e6ad3e Fix Proxmox agent install token isolation
Keep Proxmox setup tokens node-scoped so cluster installs do not rotate one shared Pulse API token.

Suppress command-enable config for tokens that cannot register command channels and keep reusable installer tokens out of agent:exec.
2026-07-05 17:21:34 +01:00
rcourtman
cf6161e035 Update release control to use main 2026-07-05 15:20:37 +01:00
rcourtman
c384d5e0c8 Prepare v6.0.3 patch release
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
2026-07-05 11:07:27 +01:00
rcourtman
314429b879 Fix v5 agent update recovery
Issue: #1515
2026-07-05 10:08:19 +01:00
rcourtman
d029e4dc42 Fix demo fixture entitlement recovery
Seed the hidden demo fixture entitlement during stable demo updates so release builds can enable governed mock resources after runtime configuration is restored.

Keep the deployment contract and release policy checks aligned with the release-build entitlement gate.
2026-07-04 22:44:43 +01:00
rcourtman
cefc032a37 Fix patch-release follow-up regressions
Refs #1510

Refs #1501

Refs #1507

Refs #1442

- persist scoped workloads status filters across platform navigation
- derive host memory pressure from available memory
- reapply system settings after every monitor reload path
- bound PBS backup snapshot polling workers during large backup scans
2026-07-04 18:37:59 +01:00
rcourtman
6bdb3c9c8c Coordinate incident recorder async persistence
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
2026-07-04 13:13:41 +01:00
rcourtman
27cd862df3 Make global logging reconfiguration race-safe 2026-07-04 11:52:20 +01:00