Add toolPresentation.coverage.test.ts (42 cases) covering chat tool-call
input/output parsing and command-preview formatting, and
resourceDetailDrawerVmwareModel.coverage.test.ts (91 cases) covering the VMware
detail-drawer section builders. Both target previously uncovered pure functions
from the v8 branch-coverage backlog. New test files only; no source changes.
Add licensePresentation.coverage.test.ts (75 cases) and
patrolSummaryPresentation.coverage.test.ts (98 cases) covering previously
uncovered pure presentation/plan/verification functions flagged by the v8
branch-coverage backlog. New test files only; no source changes.
At warning level the used segment turns threshold-yellow, which sat
next to the amber cache segment as one indistinguishable yellow mass.
Give the cache segment a vertical-stripe SVG pattern so the used/cache
split stays readable at every severity color. Stripes are vertical
because the bar's preserveAspectRatio=none scaling would shear a
diagonal texture.
The stacked memory bar colors its used segment by threshold (green
below warning, yellow/red above), but the tooltip legend hard-coded
Used as green. At 80% used the bar read as all-yellow while the
tooltip claimed a green Used section. Derive the legend color from
the same severity as the segment.
Ollama is the zero-cost AI path but the setup row offered only a Server
URL, and Patrol then failed on models that cannot emit tool_calls
(#1463, #847, #1425, #1152, #880). Bless qwen3:8b, the model family
Ollama's own tool-calling docs are written against, verified locally
against Patrol's real preflight: qwen3:8b emitted the tool call on
every run; qwen3:4b never did (0/4), so no low-RAM tag is suggested.
- Registry: SuggestedModel/Note/Equivalents on AIProviderDefinition,
projected on /api/settings/ai providers; Ollama default model goes
llama3.2 -> qwen3:8b.
- Provider row: copyable 'ollama pull qwen3:8b' block with hardware
note, and a next-step hint when a successful test resolves a model
outside the blessing set.
- Model resolution: exact-ID blessed preference, so pulling qwen3:8b
makes it the auto-resolved Patrol model with no manual selection.
- Readiness copy names the blessed model (its contract pins landed
with 94ccc7a0c's staging; this commit restores green).
- manual_ollama_preflight_test.go is the env-gated re-blessing
harness; contracts updated for ai-runtime, api-contracts,
frontend-primitives, and the agent-lifecycle/storage-recovery
dependent boundaries; subsystem_lookup_test line pins follow the
api-contracts.md insertion.
Commit cc948b022 fixed the Patrol preflight strings that gated keyless
onboarding; this sweeps the rest. Error messages, readiness checks, and
guidance copy that still pointed at the retired Assistant & Patrol
settings page now name the real surfaces: Pulse Intelligence settings
for the area as a whole, and the Provider & Models settings page for
provider-credential guidance, matching the phrasing cc948b022
established. Pinning tests updated in step, including three
ai_handlers_test.go assertions cc948b022 had already left stale.
A fresh install with no AI provider looped: the Patrol zero state sent
users to the Patrol model check, the check told them to enable Pulse
Assistant in a settings page that does not exist by that name, and the
Patrol model field silently degraded to a bare text input with no hint
that a provider key or Ollama server was the missing step (#1463, #847).
- Preflight and readiness copy now names the real surfaces (Provider &
Models, Patrol settings) and the real first step: add an API key or an
Ollama server.
- The Patrol zero-state CTA, header Fix setup link, and readiness
banners route config-level causes (assistant_disabled,
provider_not_configured) to Provider & Models; model-level causes
keep the Check Patrol model action.
- The Patrol model field shows a linked zero-provider notice instead of
a bare text input when no provider is configured.
- The preflight result box no longer renders the same failure message
twice.
Planning, approval decisions, and execution for typed resource actions
move out of the HTTP handlers in internal/api/actions.go into a new
internal/actionlifecycle.Service owned by api-contracts. The REST
handlers become thin decode/actor/error-mapping adapters over the one
shared service, and ResourceHandlers.ActionLifecycle() exposes the same
service for in-process consumers, so a future Patrol action broker
inherits identical resource lookup, availability checks, plan hashing,
audit persistence, remediation locks, plan-drift revalidation,
execution, and terminal publication instead of loopback HTTP or a
parallel lifecycle.
Behavior is preserved: same status codes, error codes, and audit/
lifecycle persistence ordering, backed by the existing api contract
tests plus new fail-closed proofs for the service itself (unknown
resource/capability, availability refusal, unapproved execution,
remediation lock, plan drift, missing executor, missing store).
Contract text in api-contracts, agent-lifecycle, and storage-recovery
now names the service alongside actions.go and planner.go; the
subsystem registry owns internal/actionlifecycle/ under api-contracts
with a dedicated path policy; the code-standards and contract source
pins follow the moved invariants; and the subsystem_lookup line-number
pin shifts with the api-contracts canonical-files list insertion.
This is the first slice of making the typed action lifecycle the only
autonomous execution route for Patrol, Assistant, and MCP.
Covers previously-uncovered branches in proxmox backup recovery, AI finding
presentation, chat tool/transcript formatting, docker page model, alert
history/overrides models, and storage page state - 275 assertions through
public entry points, no source changes.
Commit 20b310201 renamed the Patrol provider-setup action copy
('Check model' -> 'Fix setup', 'Open Provider & Models' ->
'Check Patrol model' with href /settings/pulse-intelligence/patrol)
and updated the sibling patrol tests, but missed these four, leaving
build-and-test.yml red on main. Point them at the shipped copy.
The in-app updater validated download URL and channel but never compared
the target against the running version, so any valid older release asset
URL installed silently while the UI presented it as an update. ApplyUpdate
now rejects targets at or below the running version on both the community
and Pro broker paths before any history entry or download, with an explicit
allowDowngrade opt-in on POST /api/updates/apply for sanctioned cases.
The rollback half already existed but nothing reached it: createBackup
retains three backups, restoreBackup works, and history records BackupPath,
yet no endpoint or UI called restoreBackup. RollbackToBackup restores the
retained backup recorded on a history entry after re-validating the path
against the managed backup roots, shares the update-in-flight slot with
ApplyUpdate, records an Action rollback history entry linked to the source
update, marks that update rolled_back, streams a restoring stage through
the existing status/SSE machinery, and restarts via the exit-for-systemd
path. POST /api/updates/rollback carries it with the same RequireAdmin plus
settings:write gating as apply. Rollback is purely local, so the Pro
edition gate never applies to it.
Settings now has the update history surface that was missing entirely:
nothing called /api/updates/history before. The Updates panel lists recent
updates with a Roll back action on successful entries whose backup is still
retained, behind a confirmation dialog naming the restore version, and the
rollback rides updateStore's shared pending-apply marker for the
post-restart toast. restoreBackup also honors PULSE_INSTALL_DIR now instead
of hardcoding /opt/pulse, matching createBackup.
Contract deltas ride along: api-contracts picks up the rollback transport
and downgrade-conflict semantics, agent-lifecycle and storage-recovery pin
rollback as server self-update plumbing, ai-runtime and cloud-paid pin the
update watcher stage vocabulary as non-assistant non-paid shell chrome, and
frontend-primitives adds UpdateHistorySection as the history/rollback
presentation owner with matching architecture proofs.
Both the update banner and the Settings update panel duplicated the
confirm/apply POST with divergent error handling; the banner's failure
path was a native alert(). updateStore.applyUpdate() now owns the POST,
the shared error toast, and a persisted pending-apply marker recording
the pre-update version. On the next version fetch after the post-update
reload, the store confirms the running version moved and shows a
one-time 'Updated to vX.Y.Z' toast, clearing the marker either way.
Dead-code audit batch 5: types/responsive.ts (all 8 exports had zero
non-definition references) and the one-line
components/Settings/reportingResourceTypes.ts barrel (all consumers
import @/utils/reportingResourceTypes directly). Both were referenced
only via ?raw text imports in guardrail tests; those imports and their
assertions are dropped here in the same commit. The TYPE_COLUMN_*
contract remains pinned against the live sources
(utils/typeColumnContract.ts, utils/typeColumnDefinition.ts).
docker restart reuses the image the container was created with, so the
pulled update never takes effect. Point users at docker compose pull &&
docker compose up -d, with pull/stop/rm and re-run for non-compose
installs, matching docs/DOCKER.md.
Nightly GLM grunt wave: new Vitest coverage for pure logic that had no
test file or only shallow export coverage. 436 test cases, all green;
tsc and eslint clean. Tests characterize current behavior only; no
source was modified.
Untested modules now covered:
- api/responseUtils (19 coercion/guard fns)
- utils/workloads (8 predicates/builders)
- features/storageBackups/storageSearchQuery, storageMetricsIdentity
- utils/platformSupportManifest (12 manifest lookups)
- stores/sessionCapabilities, content/help registry
Weakly-tested modules strengthened (new *.coverage.test.ts):
- utils/format (5 formatters), features/alerts/helpers (apprise round-trip)
- utils/cloudPlans (parseCloudTier), utils/alerts, utils/agentResources
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.
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.
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).
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.
Renders UpdateBanner with a Pro runtime identity and asserts the in-app
apply affordance is gone (even when the plan reports canAutoUpdate) while the
Private Release Access portal link and archive/.sshsig steps render; a
community binary still gets the Apply button. Guards the Guard-2 downgrade fix.
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.
The Kubernetes workloads, services, and configuration tabs kept their
shared-toolbar search and status in component-local signals, so the saved
views shipped with the Docker containers work could never capture a -term
exclusion. The shared toolbar now reads search (q) and status from the URL
with replace-style writes, mirroring the Docker containers table, and the
namespace facet key moves onto new KUBERNETES_QUERY_PARAMS constants. The
shared Workloads surface already URL-backs q/status in
useWorkloadsControlsState; a new test pins that behaviour instead.
Resetting filters now clears every param in one setSearchParams write.
Consecutive writes each merge against the pre-navigation URL because the
router commits inside an async transition, so the Docker containers
clear-all (search, status, host as three writes) resurrected the params the
first writes had just cleared; it now issues a single combined write too.
New tests cover the Kubernetes URL filters end to end and the single-write
reset on both platforms.
Mobile users could not save, apply, or set default views on any
savedViewsKey surface because SavedViewsMenu only rendered in the
desktop controls row. Render it in the mobile chip row, show that row
when a surface has saved views even with zero menu filters, drop the
duplicate Reset the wider chip row introduced, and left-anchor the
dropdown below the sm breakpoint so it stays on screen at 375px.
Support requests keep landing for a way to hide noisy containers (one-shot
jobs, helpers) from the Docker dashboard. Free-text search across the
platform tables now understands -term exclusions via a shared
splitSearchExclusions helper in searchQuery.ts, wired into the Docker,
Kubernetes, generic platform, and Workloads search predicates. Workloads
keeps its comma OR semantics; exclusions apply globally.
The Docker containers table now URL-backs its search (q) and status params
alongside the existing host param, so saved views capture the whole filter
state and a default view with exclusions keeps unwanted containers hidden
on every visit. Search tips on the containers toolbar document the syntax.
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.
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)".
Settings > Alerts > Systems gains a 'Disk temperature by type' editor
(NVMe/SAS/SATA trigger degC) backing the existing per-type alert
thresholds that were previously hardcoded server-side. Saving alert
settings now round-trips diskTempByType and diskFillByType instead of
silently resetting them to defaults.
Disk temperature colors in the physical disks table, pool detail linked
disks, disk detail cards, and the Machines temperature fallback now
resolve from the live alert config per disk type (warning at trigger
minus the 5 degC hysteresis margin) instead of hardcoded 50/60 cutoffs.
Closes#1540
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.
The MSP report scheduling form used raw native <select> elements,
failing the settings architecture guardrail that keeps Settings
selects on the shared labelled primitive. Cadence, Weekday, Format,
and Delivery now render through FormSelect, which also links each
label to its control.
Settings never rendered the oidc.scopes field, so admins could not
request an extra group scope and the authorization request stayed at
the default openid profile email. Adds the Scopes input to the OIDC
create/edit modal, points the Groups Claim help text at it, and covers
the create/edit round-trip with model and panel tests.
References #1535.
A Relay customer who enables Remote Access and generates a pairing QR
has nothing to scan it with: Pulse Mobile is early access and its
install links live on the authenticated download page, which nothing
in the pairing flow mentioned. Add the pointer to the pairing help
text, pin it in the panel contract test, and give docs/RELAY.md the
same install path instead of 'join early access when available'.