The Patrol lifecycle work renamed the queued-fix copy (Review fix ->
Recover action, proposed fix -> typed action, and the waiting-for-
approval summaries) and reshaped ApprovalSection's badge constant, but
the unit tests still asserted the old strings and identifiers, leaving
build-and-test red on main since last night. Expectations now match the
shipped source. ApprovalSection kept MetadataBadge and shared Button
variants, so the primitive guardrails re-pin to its current shape
(BADGE_PROPS, no warningSolid variant) rather than flagging a real
regression.
The public pricing surface and in-app plan-selling copy now say
"Patrol investigates issues and explains the root cause" and
"Patrol applies safe fixes and verifies the result". Rename the
licensing catalog DisplayName/ComparisonName pairs to the title-cased
forms, align the ai_autofix UpgradeReason verbs (apply/verify the
result), regenerate the frontend catalog, and update the Go, vitest,
and Playwright pins plus README and PULSE_PRO docs.
The public pricing surface (pulserelay.pro and the license-server pricing
model, pulse-pro 3d9e365/2da1290) now sells Pro with the real Patrol mode
names and plain-English job framing. The in-app plan cards still used the
old taxonomy (Choose Patrol mode, Patrol investigates issues, Patrol
handles safe fixes) and 'admin controls' phrasing. Pro plan-selling copy
now matches the public wording: mode names spelled out (Ask first, Safe
auto-fix, Autopilot), investigation explains the root cause, safe fixes
are verified, and RBAC/audit/reporting/agent profiles are 'team controls'.
Functional CTAs (the Choose Patrol mode link) and canonical mode labels
are unchanged. Test pins updated.
The Go-side feature catalog names (Patrol Investigates Issues / Patrol
Handles Safe Fixes) still carry the old phrasing and are a separate
follow-up since they regenerate from pkg/licensing.
The document root kept a forced scrollbar (html { overflow-y: scroll })
from before the app-scroll-shell owned scrolling. The root never
scrolls, so the rule only reserved a dead 11px gutter at the right
viewport edge and pulled every fixed right-anchored element off the
true screen edge - parking the desktop Assistant launcher tab under
the scroll shell's own scrollbar, which hit-tests above descendants.
Net effect: the tab's right quarter plus the whole screen-edge strip
silently swallowed clicks, exactly where an edge-docked control gets
aimed at.
Drop the legacy root-scrollbar rule and portal the launcher out of the
scroll shell so its full face receives clicks, flush to the real edge.
Verified live: elementFromPoint returns the button across its entire
width including the last pixel column, and an extreme-edge click opens
the drawer.
The TrueNAS/vSphere sorting adoption (cf0f027b6) moved the activity
table's When header onto PlatformSortableTableHead, so the guardrail's
literal getPlatformTableHeadClassForKind('numeric-value') pin no longer
matches. The alignment intent survives as the sortable head's
kind="numeric-value" prop; pin that form instead. The metadata-badge
and Button guardrail failures predate the sorting commits and are left
for their owner.
v5 parity follow-up to 20cccc1a9, completing the adoption sweep after the
Kubernetes pass (f66c4b46b). Adopts createPlatformTableSortState +
PlatformSortableTableHead across the TrueNAS systems / apps / services /
network shares / protection / storage topology / VMs tables and the vSphere
hosts / datastores / networks / activity tables: per-table storage keys,
descendingFirst on metric, count, and timestamp columns, the built-in order
preserved until the user sorts, and canonical kind-based alignment via the
sortable head. Composite summary columns (inventory, ports, images, access,
clients, devices, flags, hosts, consumers) stay non-sortable.
The TrueNAS storage topology sorts sibling groups and re-emits the tree
depth-first, so pools re-order against pools and every dataset or disk stays
under its parent; a new sort test pins that plus descending-first metrics and
persistence.
ProxmoxNodesTable drops its bespoke non-persisted createSignal sort for the
same fabric, so the nodes sort now survives reloads like every other platform
table. Its existing sort assertions pass unchanged; the suite now clears
localStorage between tests since the sort persists by design.
Also repairs the platformOverviewLayout guardrails at HEAD: f66c4b46b landed
with the TrueNAS/vSphere head assertions already flipped to the kind form and
the Kubernetes assertions still on the legacy form (a staging race between
parallel agents); this commit carries the version with both sets on the kind
form, which is green against the migrated tables.
v5 parity follow-up to 20cccc1a9 (v5 KubernetesClusters.tsx persisted
k8s-sort-key/k8s-sort-dir sorting; v6 shipped fixed ordering). Adopts the
shared platform-table sort fabric (createPlatformTableSortState +
PlatformSortableTableHead) across the clusters, nodes, pods, deployments,
controllers, services, storage, config, autoscaling, and networking tables:
per-table storage keys, descendingFirst on metric/count columns (pods Ready
stays ascending-first so the least-ready pods surface), the status-first
compare preserved as the base order until the user sorts, and canonical
kind-based alignment via the sortable head's kind prop. Composite summary
columns (ports, selectors, labels, access/policy, bounds, detail) stay
non-sortable. TrueNAS/vSphere adoption is landing separately.
v5 parity: v6 platform tables shipped with fixed status-first ordering and
no sortable headers. Adds opt-in sortable-header support to the shared
platform table fabric (createPlatformTableSortState + PlatformSortableTableHead
in sharedPlatformPage.tsx): click cycles first-direction -> flipped -> built-in
order, state persists per table via usePersistentSignal, missing values sink
to the bottom, aria-sort and arrow indicator match the workloads table, and
alignment stays on the canonical kind helpers.
Adopted across the Docker tables (containers, hosts, images, services,
volumes, tasks, swarm nodes, secrets, configs). Container sorting applies
within host groups and stays orthogonal to grouping, unlike v5's
sort-by-host-equals-grouping design. Kubernetes and TrueNAS/vSphere adoption
are follow-ups.
v5 parity regressions reported by a Pro customer after the v6.0.5
upgrade:
- The Docker containers table lost the grouped view: add a
Grouped/List segmented control (same fabric as the workloads
table), grouped by host with count badges and host web links,
persisted locally and defaulting to grouped for multi-host fleets.
- Container web-interface links were buried in the detail drawer's
access section: the container name now renders through
WebInterfaceNameLink, linking straight to the resource customUrl
like VM/LXC guest names do. Applies to all docker native tables
via the shared name cell.
Add truenasPageModel.coverage2.test.ts (93 cases; status mappers, incident/rollup
rows, topology cycle-termination, search tokenizers) and
agentMachineTableModel.coverage2.test.ts (55 cases; sortAgentMachines across every
sort key, RAID summary/detail builders, disk/network/thermal accessors). Target
still-uncovered functions from the v8 branch-coverage backlog; new test files only.
Settings showed PVE cluster members as read-only rows, so a member whose
discovered address is unreachable from Pulse (internal cluster network IP,
stale DNS) had no manual fix; only agent re-registration could adopt a
better IP, which leaves agent-less members stranded. Surfaced in the
"Install issues with V6" support thread.
The node editor now lists cluster members with a per-member connection
address field that writes ClusterEndpoints[n].IPOverride, the field
re-discovery already preserves and EffectiveIP already prefers at poll
time (editing Host would be clobbered on the next cluster refresh).
PUT /api/config/nodes/{id} accepts write-only clusterEndpointOverrides
entries; only changed members ride the payload, an empty value clears
the override, and unknown members are rejected. The configured-nodes
cache mirrors saved overrides instead of spreading the write-only
payload field onto node config state.
A Docker install of the Pro runtime had no sane update path: the binary
self-updater refuses to run in a container, the customer compose file pins
the previous image digest (so `docker compose pull` never updates), and the
in-app guidance showed community `docker pull rcourtman/pulse:<tag>` commands
that silently downgrade the container to the community build - the same
failure mode 93bccaff1 fixed for binary installs.
The update check on the Pro binary already fetches the license server
download broker manifest (GET /v1/downloads/pulse-pro), which carries the
digest-pinned image ref plus ready-to-run compose commands. Relay that block
on UpdateInfo.dockerUpdate for Docker deployments, behind the existing
stable/rc channel guard, failing closed unless both compose commands
reference the digest-pinned ref (never a mutable tag).
Settings -> Updates now renders those commands as copy blocks when an update
is available, and the idle Docker box, the update banner, and the docker
update plan suppress every community-image command whenever the compiled
runtime identity is pro.
Contracts: api-contracts (update transport dockerUpdate block),
deployment-installability (Pro Docker update path), frontend-primitives
(install guide Pro-runtime guidance), with transport and settings-shell
proofs extended accordingly.
Add resourceStateAdapters.coverage2.test.ts (87 cases) covering the canonical
resource/platform mappers (canonicalizeLegacyPlatformData, nodeFromResource,
buildMemory/buildDisk, PBS/PMG instance + job mappers, merge helpers) not
reached by the existing coverage test. Targets still-uncovered functions from
the v8 branch-coverage backlog; new test file only.
Execution posture is now profile-owned through the core-only,
never-serialized tools.ExecutionProfile. Both Patrol profiles are
non-interactive, deny infrastructure mutations, and clear any inherited
autonomous mode. Detection restricts pulse-state mutations to an
explicit allowlist of the finding lifecycle tools - a blanket
pulse-state allowance would also permit alert dismissal and knowledge
writes - while investigation denies all pulse-state mutations, keeping
it structurally read-only. InvocationPolicy gains the allowlist and
Allows() is now tool-name-aware.
Chat turns build ONE effective request executor (control level,
autonomy, profile, resolved context) BEFORE provider projection and
clone that executor per provider attempt, reversing the previous
project-from-base-then-clone order so the offered schema and the
runtime boundary always agree. Scheduled Patrol (ExecutePatrolStream)
now runs under the detection profile instead of autonomous mode -
closing its direct view of Docker/Kubernetes mutation subactions - and
ListAvailableTools projects through the identical path.
toolsForExecutionMode's mode booleans are replaced by the profile on
the executor itself.
Non-interactive profiles independently hide pulse_question from the
manifest AND runtime-block it before the interactive-call-set special
case: a fabricated question call returns a non-interactive error
without emitting a waiting event, and sibling tool calls from the same
provider turn keep processing. Approval waits never block for
non-interactive profiles (they queue), and the tool-only-turn wrap-up
guardrail is interactive-profile-owned instead of keyed on autonomy.
The system prompt describes detection and investigation modes directly
rather than claiming controlled or autonomous execution; the
investigation prompt directs the model toward typed action proposals.
Proofs: detection allowlist enforcement (alerts resolve and knowledge
remember blocked, finding tools allowed, projection agrees),
investigation structural read-only (patrol mutation tools dropped from
the manifest and blocked at runtime), profile clone isolation, profile
prompt modes, and question-tool hiding across profiles.
Add alertsConfigurationModel.snapshot.test.ts (83 cases, exercising the
factory-fallback branches of readAlertsConfigurationSnapshot and
buildAlertsConfigurationPayload) and alertOverridesModel.projected.test.ts
(81 cases, covering buildProjectedOverrides across every resource-type switch
branch plus the ID-candidate builders). Second-pass coverage targeting still
uncovered functions from the v8 branch-coverage backlog; new test files only.
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.
Once anything requests a test module through the Vite dev server it joins
the client module graph, and every later save of that file dead-ends HMR
into a full page reload broadcast to all connected clients. With parallel
agents saving test files continuously, the dev UI reloaded out from under
interactive sessions every few seconds during bursts (31k+ reloads in the
hot-dev log), which read as "tabs sometimes don't load when clicked".
Ignore test files in the dev watcher, scoped off vitest's test mode so
watch-mode re-runs still see changes. Verified live: test-file touch no
longer reloads a connected page, source HMR unaffected, vitest run green.
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