Commit Graph

8827 Commits

Author SHA1 Message Date
rcourtman 6be2af1c19 Let providers evaluate MSP without asking permission first
Two mandatory round-trips stood between an interested MSP and their first
screen, and neither was technical.

setup.sh required four image digests shipped as literal <pin>
placeholders, so the only way to get them was to ask. All four images are
publicly readable, so there was never anything to hand out. setup.sh now
resolves each blank pin to an immutable digest from its published tag via
buildx imagetools and writes it back to .env; hand-set values are left
alone.

setup.sh then died outright without a licence file, so nobody could start
the stack, create a workspace, or see the portal until a human minted a
licence for them. The control plane already ran unlicensed via
ProviderMSPPlanSourceEnvFallback; only the installer refused. A licence
path that is set but missing is still a hard failure, since that is a
misconfiguration rather than a choice.

Unlicensed now means evaluation rather than the cheapest paid tier. The
env fallback defaulted to msp_starter, handing every unlicensed
deployment the full 5-client Starter allowance and leaving no boundary
between evaluating and buying. Adds msp_eval at 2 workspaces: same
capabilities, smaller cap, not purchasable, not on the public ladder.

An isolation guarantee is the one claim a provider cannot evaluate from a
screenshot, and both MSP leads this year went quiet at exactly this step.

Contracts: cloud-paid records the unlicensed plan rule and the
strictly-below-paid invariant; deployment-installability records
credential-free, correspondence-free installability.

Verification: TestMSPEvalCapStaysBelowCheapestPaidTier,
TestCanonicalizePlanVersion_MSPEval,
TestProviderMSPSetupScriptSupportsUnlicensedEvaluation. The last was
negative-tested by reintroducing a <pin> placeholder and confirming it
fails. ensure_image_pins exercised against the live registries.
licensing, cloudcp, control-plane and installtests all green.
2026-08-03 16:31:24 +01:00
rcourtman f668614afd Add a portal preview scenario fit to be seen in public
The existing preview scenarios exercise edge cases and are named for it
(MSP Test Workspace A/B/C), and none of them set the alert counts, so
the Alerts column renders 'unknown' and the fleet rollup is invisible.
That is fine for a dev harness and useless for showing anyone what the
provider portal does.

Adds a 'demo' scenario: six settled clients with invented names on
reserved example domains, one critical and one warning, so the portal
surfaces a real 'Next: Review Hilltop Care' and the fleet summary reads
the way it does on a normal morning. This is what the pulserelay.pro MSP
walkthrough is recorded against.

Preview harness only, no production path touched. Portal suite green,
103 tests.
2026-08-03 15:34:20 +01:00
rcourtman 8365780282 tighten mobile filter action layout 2026-08-03 15:33:07 +01:00
rcourtman 85b0530439 standardize filter bar label presentation 2026-08-03 15:09:09 +01:00
rcourtman 3afc8bda15 align Add filter controls 2026-08-03 14:50:42 +01:00
rcourtman 37dfdf95ad fix platform table filter reset 2026-08-03 14:18:44 +01:00
rcourtman e3fdfc98b4 fix alert history filter reset 2026-08-03 12:22:19 +01:00
rcourtman 2402858e3c fix Proxmox backup saved views 2026-08-03 12:03:34 +01:00
rcourtman 59eb6dd68f fix machines saved view consistency 2026-08-03 11:34:13 +01:00
rcourtman d755c61f6a Keep all-scope mobile filter labels meaningful 2026-08-03 10:21:03 +01:00
rcourtman 419385ccfa Refresh the frontend bundle-size baseline
The FilterBar chunk grew past its budget as the saved-views and View
controls work landed, so the bundle-size gate held Build and Test red once
the lint and docs-sync failures ahead of it were cleared. The growth is
accounted for by that merged work rather than accidental bloat, and total
JS and CSS both stayed within budget throughout.

Regenerated with vite build plus check-bundle-size.mjs --update-baseline.

Contract-Neutral: build budget baseline, no contract delta
2026-08-03 10:09:55 +01:00
rcourtman 7f74e04be6 Ship the agent substrate doc the Settings panel links to
AGENT_SUBSTRATE_DOC_URL resolves to /docs/AGENT_SUBSTRATE.md and is
rendered as a link in AgentIntegrationsPanel, but the file was never
copied into frontend-modern/public/docs, so following it returned a 404.

Ship the doc and cover it in the sync test alongside the others.
2026-08-03 09:55:16 +01:00
rcourtman daecc6d456 Sync shipped docs with the AI transparency link
17f113708 added the AI-Assisted Development entry to docs/README.md but
left the shipped copy under frontend-modern/public/docs untouched, so the
docs-sync test failed and Build and Test stayed red once the lint error in
front of it was cleared.

Copy the README across and ship AI_TRANSPARENCY.md too, otherwise the new
entry resolves to a missing /docs/AI_TRANSPARENCY.md in the in-app viewer.
Add the pair to the sync test so the two cannot drift again.

Contract-Neutral: shipped documentation sync, no contract delta
2026-08-03 09:54:28 +01:00
rcourtman 2a1bf28394 Create the config directory before enabling auto-updates
Reinstalling after removing /etc/pulse reaches setup_auto_updates before
setup_directories has recreated the config directory. The system.json
write then failed with "No such file or directory" while the run still
printed that automatic updates were enabled, so the installer reported a
state it had not reached.

mkdir -p the config directory first, and fall back to disabling
auto-updates when it cannot be created.

Contract-Neutral: installer behavioural fix, no contract delta

Refs #1663
2026-08-03 09:47:03 +01:00
rcourtman 99ad8c2c4b Skip virtual block devices in host disk I/O collection
collectDiskIO hand-rolled a partial exclusion list (loop, ram, dm-) while
the two sibling paths, agent SMART collection and the server-side resource
registry, both use fsfilters.IsVirtualBlockDevice. ZFS zvols were therefore
collected as if they were physical disks: a Proxmox host with ZFS-backed
guest storage exposes one zd<N> device per zvol, so a few hundred guests
produced a few hundred phantom disks in every agent report, driving
sustained metrics and resource_changes growth.

Route the filter through the canonical helper so zd, zram, nbd, rbd, drbd,
md, pmem, vd and xvd are all excluded. md and dm- aggregates additionally
restated the I/O of their own physical members, so counting them alongside
those members double counted host disk I/O.

Refs #1671
2026-08-03 09:43:11 +01:00
rcourtman bab70cc177 Fix Solid reactivity lint error in ThresholdsTable
The threshold platform tab builder destructured its map callback argument,
which eslint's solid/no-destructure flags because the callback returns JSX.
Build and Test has been red on main since c35ab1ad1.

Use property access on the definition object instead.

Contract-Neutral: presentation-only lint fix, no contract delta
2026-08-03 09:40:23 +01:00
rcourtman c35ab1ad1c Separate threshold platform navigation from filters 2026-08-03 08:54:14 +01:00
rcourtman 1b2e9866a8 Move durable Storage layout into View 2026-08-03 01:24:41 +01:00
rcourtman b2e5ad6061 Fix mobile Saved views positioning 2026-08-03 01:03:34 +01:00
rcourtman 245177e531 Restore backend lint to green: real dedup + errcheck idiom fixes
golangci-lint had accumulated 12 findings since 5abb2d8f4. All fixed with
real dedup (no nolint suppressions) and the repo's existing errcheck idioms:

- dupl internal/monitoring: docker/host identity-conflict trackers were
  structural clones; extracted a shared identityFlapTracker core with a
  domain-neutral identityConflict result. Per-domain files now hold only
  the window const and the model translation. Tracker-behavior tests
  consolidated into identity_flap_tracker_test.go; Monitor-level
  translation and Apply*Report integration tests remain per domain.
- dupl internal/api/router.go: VM/container workload chart loops shared a
  16-line live-fallback block; extracted guestChartSeriesWithLiveFallback
  over a guestLiveMetricsView interface both views satisfy.
- dupl internal/storagehealth/risk.go: SMART attribute copying extracted
  into applySMARTAttributes shared by both assessors (same
  *models.SMARTAttributes type on both inputs).
- errcheck pkg/audit/sqlite_logger.go: three defer tx.Rollback() sites
  now use the repo-wide defer func() { _ = tx.Rollback() }() idiom.
- errcheck telemetry/notifications tests: send() errors now fail the
  test; queue.Stop() uses the package's _ = idiom.

Full test suites pass for all six touched packages.

Contract-Neutral: lint-hygiene restoration: dupl dedup (identical logic extracted to shared helpers) and errcheck idiom fixes; no public-contract or behavioral delta
2026-08-03 01:01:20 +01:00
rcourtman f550c55443 Centralize FilterBar View controls 2026-08-03 00:48:57 +01:00
rcourtman 62ca94d6ea Note the dedicated triage bot identity in the AI transparency statement 2026-08-03 00:29:02 +01:00
rcourtman 46e1f4ae54 Centralize platform table View controls 2026-08-03 00:23:00 +01:00
rcourtman 8be236e99a gofmt: realign map literal in memory_availability_test.go
Mechanical formatting drift surfaced by make format; no code change.
2026-08-03 00:17:50 +01:00
rcourtman 832ca35db5 Remove dead resource_metadata table from unified resource store schema
The resource_metadata table (canonical_id-keyed custom_url/custom_name/
notes/tags) had no readers or writers anywhere in pulse or
pulse-enterprise; guest metadata is owned by the JSON-file store in
internal/config/guest_metadata.go keyed by instance:node:vmid. Stop
declaring the dead table and its index in fresh schemas. Deployed
databases are left alone deliberately — no DROP TABLE migration, since
a stale unused table is harmless. store_test.go now fails if a fresh
store's sqlite_master reintroduces resource_metadata.
2026-08-03 00:17:50 +01:00
rcourtman 739ccad864 Move Docker layout controls into View 2026-08-02 23:52:18 +01:00
rcourtman b109c1230f Fix filter toolbar control inconsistencies 2026-08-02 23:40:13 +01:00
rcourtman 3e3839022d Resolve guest finding references before operator-state lookups
The findings runtime keys resources by whatever ID the producer used:
unified-derived findings carry the hashed canonical resource ID, but
Patrol guest inventory rows carry the node-scoped Proxmox source ID
(instance:node:vmid). The operator-state provider closure looked the
reference up in the unified store directly, which keys by canonical ID
only, so maintenance windows, intentionally-offline intent, and
criticality silently never reached guest findings - Patrol could
auto-propose fixes for a guest the operator had flagged as in
maintenance, and the suppression auto-ack never fired for guests.

A reference that misses the store now resolves through the registry
(GetByReference, which after the node-independent guest identity change
also covers retired canonical-ID eras and node-scoped guest source
references) and retries under the canonical ID. Direct canonical hits
stay as cheap as before; the resolution hop only runs on a miss.

Contract-Neutral: operator-state provider ref-resolution fix: router.go closure bug fix with api-contracts delta staged; dependent subsystem contracts (agent-lifecycle, performance-and-scalability, security-privacy, storage-recovery) have no behavioral delta
2026-08-02 23:35:35 +01:00
rcourtman 2c916a3436 Harden saved view popover interactions 2026-08-02 23:14:32 +01:00
rcourtman 09b6de599e Fix Workloads attention status filtering 2026-08-02 22:28:36 +01:00
rcourtman bacff3e2eb Align Proxmox totals with filtered workloads 2026-08-02 21:55:18 +01:00
rcourtman c143a7cf32 Bound search history popover width 2026-08-02 21:31:20 +01:00
rcourtman b3dbc26d54 Hide exhausted Add filter control 2026-08-02 21:17:18 +01:00
rcourtman 1ed7004d06 Refine Proxmox workload view controls 2026-08-02 20:47:12 +01:00
rcourtman e74fea4bbc Give Proxmox guests node-independent canonical identity
A guest's unified canonical ID hashed its node-scoped source ID
(instance:node:vmid), so a live migration to another cluster node
re-minted the resource and orphaned every operator-owned row keyed by
the old ID: explicit availability check links (fail-closed by design,
the reported symptom in #1669), alert overrides, operator state, action
audits, manual links, and recovery subjects. VMIDs are unique within a
cluster, so guests now derive their canonical ID from instance+VMID
("proxmox-guest:<instance>:<vmid>") and keep it across migrations. The
guest-metadata half of #1669 was fixed separately at the metadata-store
layer.

Existing installs converge through record-declared succession: ingest
declares the retired node-scoped IDs superseded for every node the
instance currently knows (current names plus native aliases), so rows
orphaned by pre-upgrade migrations also re-key. Successions are now
recorded durably in a canonical_id_successions table, which memoizes
the re-key (steady-state rebuilds re-declare the same eras every tick
without touching SQL) and lets change-journal reads merge retired guest
eras the way pin EraIDs do for hosts. The succession re-key also covers
manual link/exclusion rows.

Availability links resolve retired canonical IDs and old-node source
triples through a registry superseded index plus guest-triple parsing
(persistence keys only, ambiguity fails closed), and the stored
LinkedResourceID re-homes to the current canonical ID on the
alert-migration cadence. Recovery subjects converge on the same
derivation via CanonicalSubjectResourceID, the mapper's registry-miss
fallback, node-independent external guest keys, and the store's startup
backfill, which also sweeps posture rows stranded under retired subject
keys. Metrics history and frontend row identity key off the node-scoped
source ID and are deliberately unchanged.
2026-08-02 20:30:38 +01:00
rcourtman 024aa16d92 Enforce browser verification for frontend changes 2026-08-02 20:22:36 +01:00
rcourtman 3adae11919 Point CONTRIBUTING at the AI transparency statement 2026-08-02 20:11:26 +01:00
rcourtman 17f113708e Link AI transparency statement from the docs index 2026-08-02 20:03:57 +01:00
rcourtman def11f39ef Add AI transparency statement
Public declaration of how AI tools are used to build and maintain
Pulse, linked from the README documentation list and the FAQ.
Automated triage replies now carry a disclosure footer per the
policy this document sets.
2026-08-02 20:01:22 +01:00
rcourtman 42f5a5c44d Simplify workload view controls 2026-08-02 19:59:37 +01:00
rcourtman f92bb3b527 Fix host-relative memory trends 2026-08-02 19:07:38 +01:00
rcourtman c8ff52339f Rotate configuration backups instead of growing without bound
Every unattended update created another full config snapshot under
config-backups (or next to the config dir) and nothing ever pruned old
ones, so small root filesystems filled up within days (#1646, reported
on the hardened-unit fallback path where snapshots land under the
install dir). backup_existing now keeps the five newest snapshots and
removes the rest after each successful copy.

Contract-Neutral: installer config backup rotation; shell-only fix, no runtime contract
2026-08-02 18:56:05 +01:00
rcourtman 5b86a19ebc Treat the ZFS ARC as reclaimable cache in TrueNAS memory
TrueNAS reserves most RAM for the ZFS ARC and the kernel's MemAvailable
does not count ARC even though it shrinks under memory pressure, so
memory derived from total minus available read ~95% used on every ZFS
system and masked the real pressure (#1668). The realtime telemetry
Pulse already subscribes to carries arc_size, so parse it, subtract the
ARC from effective used the same way node accounting treats buffers and
page cache, and surface it through the existing agent memory cache
field. The TrueNAS systems table stacks the ARC as a reclaimable cache
segment with a Used with ARC cache tooltip row, and the reporting
history query now fetches the arcsize graph so the memory history
subtracts the ARC per point instead of contradicting the current value.

Contract-Neutral: TrueNAS memory accounting treats ZFS ARC as reclaimable cache; existing cache field, no schema change
2026-08-02 18:56:05 +01:00
rcourtman aad62ff323 Follow guest metadata across cluster node migrations
Guest metadata (web interface URL, tags, notes) is keyed by the
node-scoped guest ID, so a VM or container migrating to another node in
the same cluster orphaned everything attached to it (#1669). VMIDs are
unique within a Proxmox cluster, so when the exact ID and the legacy
formats all miss, an entry for the same instance and VMID under a
different node is the same guest and migrates to the new node-scoped ID.

The explicit availability check link stores the hashed canonical
resource ID, which also embeds the node; that half needs a
node-independent canonical guest identity and is tracked separately.

Contract-Neutral: guest metadata follows node migration; behavioral bug fix, no wire contract or payload change
2026-08-02 18:56:05 +01:00
rcourtman fbf1da43e4 Heal forked host identities after a hostname rename
A hostname change on the same machine and token forked the host module
onto a suffixed identity to stay safe against cloned VMs sharing a
machine ID (#1584), but the fork never healed, so one machine stayed
split into two agent identities forever. In unified installs the
workload modules keep reporting under the base agent ID, so Agent
Doctor flagged both halves with no way to merge them (#1667, Home
Assistant add-on with agent_hostname set after first boot).

Once the pre-rename record has stopped reporting for three health
windows the collision is proven to be a rename, not a clone: the stale
record and the forked identity are superseded and the binding returns
to the base identity. A rename discovered while the old record is
already stale now adopts the base identity without forking at all.
Live clones keep reporting under the old hostname, so they never
match the heal condition and stay forked.

Contract-Neutral: host agent identity rename heal; behavioral bug fix, no wire contract or payload change
2026-08-02 18:56:05 +01:00
rcourtman b92615e44a Fix Proxmox workload type filtering 2026-08-02 18:24:09 +01:00
rcourtman 192aee6acc Trust newly joined cluster members under fingerprint pinning
A node joining a PVE cluster after setup could never be trusted when the
primary was fingerprint-pinned (#1664). Two independent gaps: cluster
discovery validation only relaxed TLS when client construction failed,
but a pinned-fingerprint mismatch surfaces from the first API call, so
the member was judged not-a-Proxmox-node and its captured fingerprint
discarded; and the cluster client's TOFU refresh only ran for endpoints
that already had a per-endpoint fingerprint, handling rotation but never
first trust. Validation now retries with the member's own captured
fingerprint before rejecting it, TOFU capture runs on first use, and
discovery failures now distinguish DNS, refused, timeout, and TLS
causes in the endpoint error instead of one generic message.

Contract-Neutral: cluster discovery TLS validation bug fix; no wire contract or payload change
2026-08-02 18:11:08 +01:00
rcourtman 2541e4b6a7 Run the Windows installer TLS callback off a compiled type
install.ps1 set ServicePointManager's certificate callback to a
PowerShell scriptblock. ServicePointManager can invoke that callback on
a worker thread that has no runspace, where a scriptblock delegate
fails closed, so agent downloads died with a TLS error even though the
certificate policy would have accepted them (hit by an rc.6 tester with
Skip TLS verification enabled). The copied install command already
migrated to a compiled validator for exactly this reason in rc.6; the
downloaded script now does the same. Validation policy is unchanged,
with fingerprint pinning first, then insecure, then clean chains, then
the custom CA. Verified with pwsh covering all four policy branches
plus the callback running on a runspace-free worker thread.

Contract-Neutral: installer TLS callback implementation moved to a compiled type; same validation policy, no payload or endpoint change
2026-08-02 13:12:17 +01:00
rcourtman 9715ce8a33 Keep the copied Windows install command on one line
Console hosts execute pasted input line by line, so the literal
newlines inside the embedded certificate-validator C# broke the copied
command in both Windows PowerShell 5.1 and PowerShell 7 (reported by an
rc.6 tester who had to join it manually before it would run). C# is
whitespace insensitive and the source carries no line comments, so the
type definition now collapses onto the command's single line, and a
test pins every command variant to stay newline free.

Contract-Neutral: copied command formatting only; same PowerShell semantics, no payload or endpoint change
2026-08-02 13:11:12 +01:00
rcourtman baca15e624 Show digest-pinned images as Pinned instead of Check failed
The agent intentionally reports the digest-pinned image sentinel for
image@sha256 references, where there is no tag to resolve against the
registry. Every surface funneled that through the error branch and
rendered a danger-toned Check failed badge for a state that is not a
failure (raised in #1666). The containers table, the images table, and
the container drawer now render a neutral Pinned state with a tooltip
saying why checks do not apply.

Mock containers also gain update check states (current, update
available, digest-pinned) so these badges are exercisable in mock mode
and on the demo, which previously never populated updateStatus at all.

Contract-Neutral: frontend presentation of the existing digest-pinned sentinel plus mock fixture variety; no wire contract change
2026-08-02 12:37:45 +01:00