Commit Graph

6468 Commits

Author SHA1 Message Date
pulse-triage[bot] 814edf7eab Integrate reviewed executable update acceptance
Retain exact commit 18df4c1b46. Executable identity and bounded successful version probes replace metadata-only acceptance, retaining rollback and prior-active restoration. Source-uy8hwdan contains passing 24 focused methods and 191 subcases with vet; enclosing exit 1 from the incorrect control count and the independent broad installer failure remain adverse evidence, not cleared.

Change-source: pulse-maintainer
2026-10-07 21:58:42 +01:00
pulse-triage[bot] cbeea20be1 Integrate reviewed preserving legacy sensor cleanup guidance
Retain exact commits 953e0c6dec and 2ffb4eb8ab. Existing cleanup guidance discloses LXC disruption, preserves recovery evidence and API access, and requires planned maintenance and real postconditions. Hash-bound source-4e_1ht9i proves the documentation controls, not native cleanup or shipped relief.

Change-source: pulse-maintainer
2026-10-07 21:58:40 +01:00
pulse-triage[bot] 9c378ffa94 Preserve reviewed monitoring repairs with current filesystem alert ownership
Additively merge upstream 1ac600ec2b without changing accepted commit identities. Compose the shared filesystem threshold resolver with the retained explicit host and linked-node override precedence and canonical metric release; extend the existing regression across configuration save and next report. Keep upstream live-config ownership and Patrol cleanup, alongside all reviewed monitoring work.

Change-source: pulse-maintainer
2026-10-07 21:15:51 +01:00
rcourtman 1ac600ec2b Judge filesystem alerts by their own threshold on config save (#2700)
Stacked on #2696, which is stacked on #2692. The unique commit is the
last one, 539eadfab2.

A config save re-judges every active alert
(`reevaluateActiveAlertsLocked`). Filesystem usage alerts were re-judged
against thresholds their evaluator never uses:

- **Agent filesystems** (`agent:<host>/disk:<label>`) were judged by the
host disk threshold. `CheckHost` judges them by the filesystem's own
override, then the `DiskFillByType` threshold for an inferred hardware
type, then the host threshold.
- **Guest filesystems** (`<guestID>-disk-<key>`) were resolved by the
filesystem's resource ID. That ID matches no `guest-disk:` override and
names no guest, so the guest's own override was missed too.

Example: a filesystem override with trigger 70 raises an alert at 80
under a default of 90. Saving any unrelated alert setting resolved the
alert and sent a recovery notification, and the next poll raised it
again as a new alert.

`CheckHost` and `CheckGuest` now resolve a filesystem's threshold
through `hostDiskUsageThresholdNoLock` /
`guestDiskUsageThresholdNoLock`, and reevaluation calls the same
helpers:
- `resolveHostAlertThresholdsNoLock` applies the host helper for agent
filesystem alerts.
- The new `resolveGuestAlertThresholdsNoLock` resolves a guest
filesystem alert's guest by the ID `parseGuestAlertIdentity` reads from
its resource ID.

Both helpers return override and per-type thresholds as owned copies, so
#2696's rule that evaluation never normalizes the live config still
holds.

`TestConfigSaveJudgesFilesystemAlertsByTheirOwnThreshold` (in
`threshold_resolution_shared_test.go`) covers four cases:
- an agent filesystem override
- an NVMe fill threshold
- a guest filesystem override
- a guest-wide override on a filesystem alert

With `host.go`, `guest.go` and `config_runtime.go` overlaid by their
origin/main or #2696 copies, all four fail at the unchanged save. `go
test ./internal/alerts/...` passes, and the new test plus #2696's
`TestAlertEvaluationLeavesTheLiveConfigToTheLock` pass under `-race`.

Known limits, unchanged by this PR:
- Custom rules that filter on live metrics still see no readings during
reevaluation.
- `pulse-relaxed` is not applied during reevaluation.
- The mock generator's dash-separated guest IDs don't parse as guest
identities.
2026-10-07 21:10:14 +01:00
rcourtman 77d2119792 Remove unread Patrol summaryStats and its audit rule (#2690)
Stacked on #2686, which sits on #2682 and #2674; its own change is the
top commit, 6d8dce2cae. It merges cleanly with main and with every other
open PR touching these files.

`usePatrolIntelligenceState()` still returned `summaryStats`, a closure
counting critical, warning and active Patrol findings plus fixed
outcomes. Its only reader was `PatrolIntelligenceSummary.tsx`, deleted
in 27ba85ddc6. A type-checker scan of the whole program (src and tests)
finds no reference to the member outside the hook's return object, and
nothing spreads, iterates or index-reads the returned state.

- Removes the closure and its return entry.
- Removes the `canonical-patrol/no-local-summary-card-presentation`
audit rule. It matched only the deleted card's tint ternaries on
`summaryStats()`, and it pointed authors at `patrolSummaryPresentation`,
which no longer holds summary-card presentation. The three
`not.toContain("summaryStats()...")` guards that pinned the same strings
go too.
- Takes `patrolSummaryPresentation.ts` off the audit allowlist. Its
remaining presenters trip no rule, and a planted violation is now
reported.
- Contract notes in patrol-intelligence, api-contracts and
frontend-primitives. The section-owner sentence that named the deleted
summary surface now names the live tab panels and scopes the
no-own-reads rule to the hook-backed surfaces.

Runtime neutrality: of 187 dist files, 94 are byte-identical and 92
differ only in chunk-hash names and SRI strings. In the AIIntelligence
chunk, after normalising hashes and short identifiers, the only residual
is the removed closure and its return entry. The bundle-size check
passes. The browser receipt covers the Patrol Inbox, Activity and
Protection tabs at 1440 and 390 px.

The `Contract-Neutral` trailer covers only the platform-support-manifest
proof that the audit script's path policy names. This change leaves that
projection check untouched.
2026-10-07 20:42:56 +01:00
pulse-triage[bot] 2d99718ace Integrate reviewed guest metadata fetch ownership
Accept Core's exact 9b77a4b6 and ec4ee724 commits after runtime, regression and retained source-u41zem5j proof review. Preserve cold-cache backoff and whole-fetch ownership while retaining deliberate invalidation recovery. Native guest recovery and protected publication remain separate outcomes.

Change-source: pulse-maintainer
2026-10-07 20:06:52 +01:00
pulse-triage[bot] 18df4c1b46 Require executable version evidence after unattended updates
Reject successful installers whose replacement cannot identify itself as the target community runtime. VERSION metadata and failed or unrecognised probes cannot establish acceptance; retain the existing verified rollback and prior-active restoration. Exercise the complete locally signed installer transaction and bounded executable read.

Change-source: pulse-maintainer
2026-10-07 20:04:44 +01:00
pulse-triage[bot] 953e0c6dec Keep legacy sensor cleanup local and preserve recovery evidence
Treat the existing helper as disruptive Proxmox host maintenance: disclose its LXC stop/start and key-removal scope, omit purge and API-account deletion, and require pre-state, private backups and actual postconditions. Exercise copied success/failure arguments with a synthetic helper and bind safety prose to current source.

Contract-Neutral: Documentation and documentation tests only; the cleanup helper, privileges and runtime contracts are unchanged.
Change-source: pulse-maintainer
2026-10-07 20:01:07 +01:00
pulse-triage[bot] e75f3e2b35 Integrate reviewed manual update channel admission
Accept the exact Delivery candidate and its complete source-s468dla1 installer proof. The final repair removes only contract EOF whitespace; installer and regression-test bytes are unchanged. Native installed recovery and protected publication remain separate.

Change-source: pulse-maintainer
2026-10-07 19:55:33 +01:00
pulse-triage[bot] ed599f541e Reconcile supplied pre-batch Patrol upstream
Preserve exact upstream history present at coordinator launch. Later upstream refresh remains with the protected publication gate.

Change-source: pulse-maintainer
2026-10-07 19:55:24 +01:00
rcourtman 48c2d6cdce Stop Patrol fetching the correlation list nobody reads (#2677)
The Patrol page fetched `GET /api/ai/intelligence/correlations` on every
mount, every 60 s poll, every return to the tab and on Retry, and the
store's dashboard bundle fetched it again for FindingsPanel's refresh.
Nothing has read the list since the Patrol supporting-context block was
removed in 27ba85ddc6; the resource drawer reads per-resource
correlations from the resource-intelligence payload.

## Change

- `stores/aiIntelligence.ts`: remove the correlation signal,
`correlations` getter, `correlationsSignal`, `loadCorrelations()`, its
org-switch reset, and its entry in `loadDashboardData()`.
- `features/patrol/usePatrolIntelligenceState.ts`: drop the read from
the Retry path's background supporting reads.
- `api/ai.ts`: remove `AIAPI.getCorrelations` (no other caller). The
backend route stays as an API surface behind `ai:execute`.
- Tests: the store bundle test now pins every AIAPI read the bundle
makes; the manual-run test stalls the post-start refresh through a
remaining read so it still proves "Starting" clears without waiting
(negative controls: awaiting the refresh, or removing it, both fail the
test). Dead correlation route stubs removed from six E2E specs.
- Contract prose in api-contracts, patrol-intelligence,
unified-resources, ai-runtime and frontend-primitives no longer says the
Patrol page or store loads or renders correlations or recent changes.
The remaining stale Patrol correlation paragraphs (patrol-intelligence
~L2135/~L2197, ai-runtime ~L6854) are rewritten by #2674; the two PRs
split hunks by agreement and merge cleanly together.

`CorrelationsResponse` stays for now:
`patrolInvestigationContextModel.ts` still imports it until #2674 lands.
It goes in a follow-up once both are on main.

## Evidence

- Demo-mode stack, headless Chromium at 1440x900 and 390x844:
correlation requests go from 1 to 0 per Patrol load, browser reload, 60
s poll and tab hide/show. Every other `/api/ai/*` read per phase is
identical; no console errors. Re-run on the final base.
- Production build: the route string is gone from the bundle;
`check-bundle-size.mjs` passes with no chunk renamed (JS gzip -0.18 kB
on the same base).
- `tsc --noEmit` passes; vitest for src/stores, src/api,
src/features/patrol, src/components/AI and the Patrol page test passes;
contract, registry and status audits pass.
2026-10-07 19:49:15 +01:00
pulse-triage[bot] ec4ee724de Reset obsolete guest metadata scheduling on invalidation
The broad guest lifecycle proof caught a recovery regression when a known unavailable agent deliberately invalidated its cache. Clear that key’s obsolete refresh/backoff deadline without clearing active fetch ownership, independent installations or Proxmox uncertainty. Retain immediate available-poll recovery and early-failure backoff as distinct paths.

Contract-Neutral: Preserve existing recovery on deliberate guest metadata cache invalidation; no guest lock, uncertainty, command, public API, configuration or duration changes.
Change-source: pulse-maintainer
2026-10-07 19:47:50 +01:00
pulse-triage[bot] 6024afe00a Integrate reviewed scoped ZFS troubleshooting
Accept product-intelligence candidate 20261007T180421Z-product-intelligence at exact tip 2209c65e8c. Preserve the existing ZFS row and health contract while replacing blanket permission grants and masked log reads with configured-token, deployment-scoped guidance. Retained source-3rssfcmh passes fifteen affected methods and rejects parent plus adverse controls; this is source help, not native recovery or publication.

Change-source: pulse-maintainer
2026-10-07 19:43:07 +01:00
pulse-triage[bot] 09784ff945 Reconcile pre-batch upstream disk history fixes
Preserve the canonical reviewed line and incorporate the upstream snapshot present at coordinator startup, 71a723abb1. Later upstream advances remain for the publication gate.

Change-source: pulse-maintainer
2026-10-07 19:42:39 +01:00
pulse-triage[bot] 545ca061d8 Remove trailing contract whitespace from manual update candidate
Remove exactly one surplus newline reported by deterministic preflight. Installer runtime, regression tests and substantive contract text remain unchanged.

Contract-Neutral: EOF whitespace only; no runtime, test or contract delta.
Change-source: pulse-maintainer
2026-10-07 19:41:37 +01:00
pulse-triage[bot] 9b77a4b6c6 Keep guest metadata fetch ownership through completion
Honour cold-cache retry backoff and retain a per-key in-flight reservation until enrichment returns, rather than treating a timed hold as completion. Avoid duplicate command sequences and stale safety-state writers while preserving existing lock, uncertainty, observation and refresh policies. Cover blocked real HTTP reads, early deferral, slot cancellation and ordinary recovery.

Contract-Neutral: Restore the existing per-key metadata reservation and retry scheduling semantics; no public API, configuration, guest command, lock, uncertainty fence, observation or numeric duration changes.
Change-source: pulse-maintainer
2026-10-07 19:39:32 +01:00
rcourtman 71a723abb1 Read controller-member disk history at its written key (#2694)
## Problem

A physical disk behind a RAID controller (target such as `megaraid,3`)
with no usable serial or WWN had its SMART history written under one key
and read under another, so its temperature and SMART charts stayed
empty.

- Host agent: writes under `HostSMARTDiskMetricID`, e.g.
`host-pve:sdd@ctrl0/megaraid,3`.
- Proxmox: writes under `PhysicalDiskMetricID`, which returns
`ProxmoxPhysicalDiskSourceID`, e.g.
`pve-node1--dev-sdx:sdx@/megaraid,0`.
- Reader: `BuildMetricsTarget` passed those source IDs, which already
name the member, to `PhysicalDiskMetaMetricID`. That function appended
the topology again:
`host-pve:sdd@ctrl0/megaraid,3:sdd@ctrl0/megaraid,3`. The same happened
in a registry rehydrated from persisted resources.

## Fix

`PhysicalDiskMetaMetricID` now follows the Proxmox writer. A fallback
that already ends in the member's topology is returned unchanged. The
topology is appended only to a fallback without it:
- a Proxmox source ID from before members were scoped, which
`PhysicalDiskMetricID` scopes the same way;
- the canonical resource ID that `PhysicalDiskView.MetricResourceID`
falls back to.

TrueNAS disks carry no controller target and are unaffected.

## Existing history

- **SMART:** no SMART writer used the doubled key, so samples already
stored become readable without a migration.
- **Disk I/O:** a linked node's agent with no SMART reading for a
member, such as a standby row, files the device's I/O under the Proxmox
disk's metrics target. That target was the doubled key. The I/O now
lands beside the member's SMART history. Samples already stored under
the doubled key are left to age out: nothing reads them, and they are
the block path's counter filed under its one merged member.
- Mock seeding keys these disks by `disk.ID`, the single-scoped key, so
it already matched.

## Proof

- `TestIdentitylessControllerMembersReadTheirWritersHistory`
(`internal/monitoring/issue1595_collection_trust_test.go`) drives the
real writers: `writeHostSMARTMetrics`, `writeHostPhysicalDiskIOMetrics`
and the Proxmox `writeSMARTMetrics`. It then queries the store at
`MetricsTarget` for five members in a live registry and in one
rehydrated through a JSON round trip. The members are two agent members
sharing a path, a lone agent member with I/O, a Proxmox member, and a
standby member merged into its linked node's Proxmox row. Before the fix
all ten targets read the doubled key.
- `TestPhysicalDiskMetaMetricIDScopesAControllerMemberOnce`
(`internal/unifiedresources/canonical_ids_types_test.go`) pins each
fallback shape against its writer: agent, smartctl-labelled agent,
Proxmox, legacy Proxmox, canonical resource ID, and a disk that is no
controller member. Before the fix, three cases failed with the doubled
key.

## Stacking

This branch sits on #2685, which sits on #2679, because both edit
`physical_disk_ids.go` and the proof file. It supersedes #2661, which
carried the same reader fix but conflicted with this stack and with
main.
2026-10-07 19:31:12 +01:00
pulse-triage[bot] 493e0b5fa9 Bind parsed manual channels through installer execution
The installer rereads saved preferences without an explicit selector. Forward the parsed stable default as well as RC, and prove the containing installer's actual channel-selection functions against the same signed fixtures.

Change-source: pulse-maintainer
2026-10-07 19:23:46 +01:00
pulse-triage[bot] 1992c681c2 Preserve reviewed monitoring maintenance with current Patrol and alert defaults
Additively incorporate upstream 843daf7ae3 without removing accepted source. Resolve the overlapping AI documentation assertion by preserving its whitespace-normalized compatibility check together with upstream dead-Patrol guidance. The focused documentation policy tests pass; browser and final-source validation dependencies remain explicit.

Change-source: pulse-maintainer
2026-10-07 19:11:35 +01:00
pulse-triage[bot] f9a0d3d95e Keep Proxmox ZFS diagnosis scoped to existing read access
Separate missing storage rows, capacity and pool health; remove blanket guessed-account ACL advice and user-only permission checks. Reuse bounded server log guidance on the actual Pulse host, preserving native health checks and private evidence.

Contract-Neutral: Existing documentation and safety controls only; no runtime, API, feature or permission contract change.
Change-source: pulse-maintainer
2026-10-07 19:09:21 +01:00
pulse-triage[bot] 589d7e18b5 Read manual update channels from complete top-level settings
Stop the generated helper before downloads when saved channel evidence is malformed or unreadable. Preserve explicit selectors, source-marker precedence and signed-installer verification; exercise the actual helper with signed local fixtures.

Change-source: pulse-maintainer
2026-10-07 19:08:04 +01:00
rcourtman 843daf7ae3 Remove dead Patrol assessment and save-failure handoffs (#2682)
Stacked on #2674 (branch `refactor/drop-patrol-context-summary`); this
branch contains that commit.

## What

Removes the two Patrol-to-Assistant handoff builders in
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`
that no production code calls:

- `buildPatrolAssessmentAssistantHandoff`: its caller, the summary
card's Discuss handler, went in eac7dfe9ef (2026-05-15).
- `buildPatrolConfigurationFailureHandoff`: its last caller went in
27ba85ddc6 (2026-06-23).

With them go `PatrolInvestigationContextSummary`, their input and output
types, and the private helpers only they used. The model drops from
2,326 to 1,257 lines.

## Why it is safe

- A TypeScript compiler export graph rooted at `src/index.tsx` shows no
reachable importer of either builder, and `git grep` finds only their
own tests.
- Production builds of the parent tree and this tree are byte-identical
across all 187 dist files (`shasum`). `check-bundle-size.mjs` passes.
- The Assistant drawer still restores saved `patrol_assessment` and
`patrol_configuration_failure` sessions. That code
(`components/AI/Chat/index.tsx`, `workflowStarters.ts`,
`internal/ai/chat/session.go`) is unchanged.

## Tests

- The removed builders' cases are gone.
- Two cases covered branches the live finding handoff still uses, so
they now go through the live builders: a typed pending action stays
approval-bound without a legacy approval id, and the resource node
qualifier plus long-line truncation.
- v8 coverage of the surviving model code matches the baseline from
before the removal. The one exception is a defensive guard the live
caller cannot reach.

## Contracts and docs

Eight contracts changed: `patrol-intelligence`, `ai-runtime`,
`api-contracts`, `frontend-primitives`, `security-privacy`,
`storage-recovery`, `agent-lifecycle` and `unified-resources`.

- No contract describes either handoff as a live producer any more.
Statements about restoring saved sessions of those kinds stay.
- Some clauses described a rendered Patrol summary card, assessment
strip or readout, score chip, header trust line, or inline save-failure
surface. They are now conditional or say what renders instead.
- The run-history handoff clauses now say the browser sends the briefing
and safe run metadata, and `internal/ai/patrol_assistant_handoff.go`
rebuilds the model context from the run ID.
- `docs/AI.md` and its public mirror pointed users at a legacy
operational score in a collapsed supporting-context section. Neither
exists any more. The section now points at **Activity > Finding options
and history > History**.
- `ai_runtime_docs_policy_test.py` is the registered proof for
`docs/AI.md`. It now pins that location and rejects the retired claim.
It also had a stale pin that has failed on main since 2789a15fe5; that
pin now matches the current Patrol mode API wording.

Prose about the remaining test-only summary presenters in
`utils/patrolSummaryPresentation.ts` is left for a separate follow-up
that removes those presenters.

## Browser verification

Receipt `frontend-modern/browser-verification/6e6cbc66d62863d1.json`.
Checked on an embedded production build on a private demo mock stack, at
1440x900 and 390x844:

- Inbox "Explain with Assistant" opens the drawer with the Pulse Patrol
attention briefing.
- The run-history "Discuss" button opens the drawer with the "Patrol run
attached" briefing.
2026-10-07 18:52:17 +01:00
rcourtman 2eef42c512 Keep Off alert defaults off after saving (#2684)
## Summary

Switching a metric Off in a **Global Defaults** row (Alerts >
Thresholds) did not stick. After Save and a reload the metric was back
On at its factory threshold and kept alerting. This affected every
section whose factory threshold is positive: Machines CPU, memory, disk
and disk temperature, Proxmox node temperature, PBS, Docker, Kubernetes,
TrueNAS, vSphere and storage.

The editors stage `-1` for Off, and the save payload passed it through
as `{trigger: -1}`. Backend config normalization reads a negative
default trigger as unset and restores the factory value. Guest and node
CPU, memory and disk escaped only because nothing normalizes them.

- `buildAlertsConfigurationPayload` now writes every Off default as
`{trigger: 0, clear: 0}`, the value the engine and normalization both
read as off. Per-resource overrides are serialized separately and keep
`-1`, which the override path stores as written. A `-1` already stored
on a guest or node default becomes `0` on the next save.
- The Global Defaults **Custom** badge now treats `-1` and `0` as the
same Off value. Without that, a fresh install showed Custom on
Kubernetes, TrueNAS Systems and vSphere, and VMs & Containers turned
Custom after any save, though nothing had changed. An unset factory
value stays distinct from Off: an unset TrueNAS disk temperature follows
Disk temperature by type.
- A row's custom-threshold marker still compares raw values. It drives
the row's revert control and override removal, so a stored `-1` override
must stay visible against an Off default. A test pins that.
- The alerts contract no longer claims Off is written as `-1`
everywhere.

## Verification

- New payload test: every section's Off default is written as `{0, 0}`
and reads back Off. It fails on `main` with `guestDefaults.cpu: expected
{ trigger: -1 }`.
- New backend test: a `{0, 0}` default stays off through `UpdateConfig`
in every section, and a host at 99% raises no CPU, memory or disk alert
when the agent defaults are off. Both fail when the defaults are written
as `-1`.
- Live on a private mock stack. On `main`, Proxmox CPU and Temp switched
Off saved as `{-1}` and `{80, 75}`, and after reload Temp read On. With
the fix:
- Machines CPU at 1% raised 8 alerts. Switching it Off stored `{0, 0}`
and resolved all 8.
  - Proxmox Temp Off resolved the node temperature alert.
  - Nothing re-fired over two minutes.
- Values stayed Off after a backend restart and a reload. Machines,
Proxmox, Kubernetes, TrueNAS and vSphere were each checked at desktop or
phone width.
- Custom badge on a fresh install: no Proxmox, Kubernetes, TrueNAS or
vSphere group reads Custom, and after an unrelated save only the group
actually changed does.
2026-10-07 18:47:09 +01:00
pulse-triage[bot] 98b92689a3 Integrate reviewed bounded agent log guidance
Preserve the exact reviewed Product Intelligence commit and retained documentation proof. Existing agent help uses passive bounded log reads and keeps failed partial output private.

Change-source: pulse-maintainer
2026-10-07 18:45:56 +01:00
pulse-triage[bot] 65bfcda7dd Integrate reviewed guest OS-info suppression repair
Preserve the exact reviewed Core commit and completed source controls. Retain completed safety suppression on later version deferral without renewing partial observations or changing retry and backup admission.

Change-source: pulse-maintainer
2026-10-07 18:45:54 +01:00
pulse-triage[bot] 615e113469 Integrate reviewed Proxmox replication help
Preserve the three reviewed Web commits and their exact source proof. Existing help separates classification from stale inventory without disruptive diagnostics.

Change-source: pulse-maintainer
2026-10-07 18:45:52 +01:00
pulse-triage[bot] ba90322535 Integrate reviewed stable release metadata admission
Bind unattended update selection to complete release objects with explicit stable maturity, while preserving signature, edition, consent and recovery gates.

Change-source: pulse-maintainer
2026-10-07 18:30:10 +01:00
pulse-triage[bot] d793f32a30 Preserve reviewed maintenance with current summary cleanup
Additively incorporate the batch-start upstream export cleanup without changing accepted commit identities. Retain monitoring, recovery and help integrations for validation and protected publication.

Change-source: pulse-maintainer
2026-10-07 18:06:41 +01:00
pulse-triage[bot] 4733e816e2 Bound passive agent log collection across help entry points
Replace indefinite journal followers, a failure-masking search and diagnostic debug/restart advice with one shared deadline-bounded reader. Preserve failed reads and both streams, identify the affected agent host and keep raw errors private. Exercise copied commands with missing-tool, partial-failure, timeout and termination controls; no native recovery claim.

Contract-Neutral: Existing agent-help recipes and executable documentation controls only; agent, installation, collection, command-channel, permission and notification runtime contracts are unchanged.
Change-source: pulse-maintainer
2026-10-07 18:06:37 +01:00
pulse-triage[bot] 7cea42a4c1 Retain guest OS-info suppression across later command deferral
Preserve completed OS-info failure or reset state when a subsequent version read pauses. Keep partial identity and observation age unchanged, honour retry backoff, and cover cold/retained caches and both HTTP poll entry points without native guest or backup changes.

Contract-Neutral: Restore existing OS-info negative-cache semantics across version deferral; no public API, configuration, admission, pause or observation contract changes. Dedicated source tests and contract clarification accompany the fix.
Change-source: pulse-maintainer
2026-10-07 18:05:31 +01:00
rcourtman dace9295c6 Remove exports orphaned by the summary component deletions (#2671)
Stacked on #2662 (its commits are ancestors of this branch, so this PR
shows them until #2662 merges).

The May cleanup deleted InfrastructureSummary, StorageSummary, the
shared summary table and the SummaryMetricCard, InteractiveSparkline,
DensityMap and SummaryPanel primitives, but left the helpers only they
called. This removes them:

- `getMetricsChartKeyCandidatesFromResource` (utils/agentResources.ts)
- `getNormalizedIdentityLookupVariants` and
`getAgentLikeIdentityAliases` (utils/resourceIdentity.ts)
- `getPBSStatusIndicator` (utils/status.ts)
- `useSummaryContextualFocusState`
(components/shared/contextualFocus.ts), plus
`filterSummarySeriesByGroupScope`, `resolveSummaryCardInteractionState`
and `SummaryCardInteractionState` that only it used
- the deprecated `ChartsAPI.getInfrastructureCharts` delegate; its
branch tests now exercise `getInfrastructureSummaryCharts`

Proof of deadness: a TypeScript import/export graph from `src/index.tsx`
finds no production importer of any removed symbol, and a repo-wide grep
finds no remaining reference. The production build differs from its
parent only in the shared chunk that carries ChartsAPI, which loses the
delegate's static method (99 bytes); after normalising chunk hashes, SRI
hashes and minified identifiers every other file is identical. Tests:
`src/utils`, `src/components/shared`, `src/api`, `src/features`,
`src/hooks`, `src/__tests__`, Workloads, Storage and Infrastructure
suites pass (only the known Mac-only `agentInstallCommand` failures).

Contracts: frontend-primitives, storage-recovery and
performance-and-scalability still described the deleted summary cards,
sparkline, density map, summary panel, sticky summary section and Jump
to row button as live owners, and credited contextualFocus.ts with the
removed hook's series filtering. Those clauses now name the live owners
(summaryCardInteraction.ts behind summaryTableFocus.ts, the storage
summary cache behind the pool Growth column, TooltipPortal for the mini
sparkline and backup activity chart tooltips), become conditional on a
summary strip returning, or go. api-contracts and unified-resources
record the removed chart client and chart-key helper.

Browser pass (embedded production build, demo mock stack, 1440x900 and
390x844): Proxmox node-to-guests reveal, guest row hover emphasis and
inline detail reveal, storage Growth column, pool row hover and inline
detail, pool-group hover preview, pin and Escape clear. Receipt:
`frontend-modern/browser-verification/232e5c23490f8cd4.json`.
2026-10-07 18:02:21 +01:00
pulse-triage[bot] df046b6f88 Integrate reviewed passive notification report guidance
Merge exact PI candidate d0a4f48bb2. Retain optional Test-versus-delivery and message-identity context without new sends, queue mutation or destination disclosure. Exact source proof and adverse controls were reviewed; no runtime or notification-delivery acceptance is claimed.

Change-source: pulse-maintainer
2026-10-07 17:58:55 +01:00
pulse-triage[bot] d0a4f48bb2 Keep notification report evidence passive and distinct
Clarify Test versus ordinary delivery and message identity in the existing optional evidence fields. Preserve original queue context without sending, replaying or exposing destination secrets; pin both form schemas and the mirrored triage guide.

Contract-Neutral: Report guidance and its static checks only; no runtime, notification delivery or intake automation change.
Change-source: pulse-maintainer
2026-10-07 17:44:15 +01:00
pulse-triage[bot] 01b910fcda Bind unattended updates to complete stable release objects
Replace line-oriented release admission with object-bound JSON maturity checks. Refuse missing flags, mixed sibling fields, incomplete payloads and redirect-only discovery; bound both API reads without ambient curl configuration. Preserve highest-stable selection, confirmed latest fallback and all existing installation/rollback gates.

Change-source: pulse-maintainer
2026-10-07 17:42:05 +01:00
pulse-triage[bot] ef222a7c7b Reclaim expired guest-command admission before capacity deferral
Count only new endpoint/VMID slots and reclaim expired idle entries when a whole alias group otherwise exceeds the unchanged 4096-entry bound. Keep busy work, uncertain completion, backup locks and single-attempt commands protected. Cover all six HTTP readers, alias reuse and fail-closed full capacity; no native recovery or reported-cause claim.

Change-source: pulse-maintainer
2026-10-07 17:41:02 +01:00
pulse-triage[bot] cb74547e15 Explain safe Proxmox replication disagreement checks
Separate job outcome classification from stale or absent inventory, point to existing native and expanded-row evidence, and keep disruptive diagnostics and broad permissions out of the help path.

Contract-Neutral: Existing mirrored help and rendered guidance checks only; replication runtime, API and authority contracts are unchanged.
Change-source: pulse-maintainer
2026-10-07 17:40:05 +01:00
rcourtman 539eadfab2 Judge filesystem alerts by their own threshold on config save
A config save re-judges every active alert. Agent filesystem alerts
were re-judged by the host disk threshold, ignoring the filesystem's
own override and the per-type DiskFillByType threshold CheckHost uses.
Guest filesystem alerts were resolved by their "<guestID>-disk-<key>"
resource ID, which matches no guest-disk override and names no guest,
so the guest's own override was missed too. A filesystem override of
70 raised an alert at 80 under a default of 90; saving any alert
setting resolved it and sent a recovery, and the next poll raised it
again as a new alert.

CheckHost and CheckGuest now resolve a filesystem's threshold through
hostDiskUsageThresholdNoLock and guestDiskUsageThresholdNoLock, and
reevaluation calls the same helpers: resolveHostAlertThresholdsNoLock
for agent filesystem alerts, and a new resolveGuestAlertThresholdsNoLock
that resolves a guest filesystem alert's guest by the ID
parseGuestAlertIdentity reads from its resource ID. The helpers return
override and per-type thresholds as owned copies, so evaluation still
never normalizes the live config.

TestConfigSaveJudgesFilesystemAlertsByTheirOwnThreshold covers an
agent filesystem override, an NVMe fill threshold, a guest filesystem
override and a guest override. With host.go, guest.go and
config_runtime.go overlaid by their origin/main or #2696 copies, all
four fail: the unchanged save resolves the alert.

Stacked on #2696.
2026-10-07 17:36:34 +01:00
pulse-triage[bot] d73614cc57 Integrate reviewed cross-installation Proxmox help
Preserve the exact Web candidate, mirrored guidance and rendered-document proof without treating documentation as native recovery.

Change-source: pulse-maintainer
2026-10-07 17:28:34 +01:00
pulse-triage[bot] 7fc545fb9f Integrate reviewed updater edition and audit ownership repairs
Retain the exact Delivery candidate and completed installer race evidence without claiming installed update acceptance.

Change-source: pulse-maintainer
2026-10-07 17:28:32 +01:00
pulse-triage[bot] 62068fce3f Integrate the upstream low-alert threshold repair
Preserve the batch-start upstream frontier alongside reviewed local maintenance.

Change-source: pulse-maintainer
2026-10-07 17:28:23 +01:00
pulse-triage[bot] f1745e47f4 Integrate reviewed privacy-safe cross-installation issue intake
Preserve the exact reviewed product-intelligence commit and its proof. Existing intake fields and mirrored triage guidance keep repeated identities distinguishable without private names or setup changes; no runtime or new product surface changes.

Change-source: pulse-maintainer
2026-10-07 17:23:14 +01:00
rcourtman 7eb32f3181 Keep a low alert default's clear below its trigger (#2678)
## Problem

Setting an alert global default to a positive trigger of 5 or less, for
example Machines CPU at 1% in Alerts > Thresholds, stored
`agentDefaults.cpu` as `{trigger: 1, clear: 75}`, a clear far above its
trigger.

The thresholds page sends `clear = max(0, trigger - 5)`, which is 0 for
those triggers. `NormalizeAgentDefaults` derived `trigger - 5`, found it
non-positive, and fell back to the factory clear (75/80/85).
`ValidateHysteresisThresholds` never checked agent CPU, memory or disk,
so the pair survived. PBS CPU/memory, node temperature and
`internal/config/persistence.go` had the same fallback. On the save
path, validation repaired PBS and node temperature, but
`LoadAlertConfig` still turned a stored node temperature clear of 0 into
75; at startup `UpdateConfig` repaired that in memory.

Per-type `DiskFillByType` / `DiskTempByType` entries were worse: any
clear ≤0 reset the whole pair. A SATA disk temperature trigger of 4 from
the thresholds page was stored as 55, and an API-written fill entry of
`{88, 0}` became `{90, 85}`.

## What a user saw

- **Firing and resolving did not change for these pairs.**
`buildCanonicalMetricSpec` drops a clear at or above the trigger, and
the reducer then clears at the trigger. That held both before and after
this change. There was no flapping and no alert that never re-armed.
- `/api/alerts/config` and `alerts.json` served the factory clear above
the trigger.
- Firing agent alerts carried it as `metadata.clearThreshold`: 75 on a
1% CPU alert, and 85 on a disk alert still firing at 81.8%.
- A per-type disk trigger of 1–5 was silently replaced by the type
default.
- Machines metric bars: `resolveThreshold` in `metricThresholds.ts`
clamps the warning level to `min(clear, trigger)`, so a 1–5% agent
trigger had no warning band. Readings below the trigger, including 0,
now show amber instead of green, which matches guest, node, Docker and
Kubernetes bars at the same setting. This one is read from the code; I
did not exercise it in a browser.

## Fix

- PBS, node temperature, agent and per-type disk defaults now use
`normalizeThresholdPointer`, the normalizer Kubernetes, TrueNAS and
vSphere already use. A missing clear sits five points under the trigger,
floored at 0. A clear at or above the trigger is repaired. Zero still
means off for global defaults, and nil or negative still takes the
factory default. For the canonical per-type keys (the types the page
edits), a non-positive trigger still resets to the type default, because
the page cannot represent a per-type Off; other per-type keys are stored
as written, as before.
- `internal/config/persistence.go` drops its duplicate
agent/node-temperature and storage helpers and calls the same normalizer
through a new `NormalizeHysteresisThreshold`. A config saved before this
fix loads repaired.
- Agent CPU/memory/disk/disk temperature are added to
`ValidateHysteresisThresholds`, as every other family already is.
- One deliberate behaviour change for invalid pairs above the margin: an
API-written `{50, 80}` is repaired to `{50, 45}`, so it now has a real
recovery band where it used to clear at the trigger. This matches what
`EnsureValidHysteresis` already does for every other family.
- An alerts contract clause records the rule.

## Verification

- `internal/alerts/config/normalize_low_trigger_clear_test.go` runs
every default family and both per-type maps through the `UpdateConfig`
normalizer chain.
- `TestUpdateConfigKeepsLowTriggerClearBelowTrigger` pins the saved
config and a firing alert's `clearThreshold`.
- `persistence_test.go` checks the written `alerts.json` itself as well
as load repair.
- All new tests fail against the previous normalizers. Coverage cases
that pinned the old fallback (including one labelled "suspected source
bug") are updated.
- `go test ./internal/alerts/... ./internal/config/ ./internal/ai/`
passes.
- Live, on private mock stacks (baseline binary vs this branch): I saved
the payload the thresholds page sends, then read back
`/api/alerts/config`.
- Baseline: agent `{1,75}`, `{3,80}`, `{5,85}`; `diskTempByType.sata`
`{55,50}` for a 4°C trigger; firing agent alerts reported
`clearThreshold` 75/80/85.
- This branch: every pair `{t, 0}`, `diskTempByType.sata` `{4, 0}`, and
alerts reported 1/3/5.
- After a restart, this branch also repaired a hand-written
`alerts.json` holding `{1,75}` and node temperature `{2,75}`.
2026-10-07 17:22:55 +01:00
rcourtman 71a470129f Stop alert evaluation writing live thresholds outside the lock
Host and guest evaluation filled in the Clear of a per-disk override's
threshold after dropping m.mu, writing through the live config's pointer.
GetConfig clones that pointer under the read lock, which does not exclude
the writer, and the saved override silently took whatever the last poll
filled in. Docker evaluation pointed into m.config.DockerDefaults, and read
the restart-loop and memory-limit settings, with no lock held while
UpdateConfig replaced m.config.

Clone the override's Disk threshold under m.mu before normalizing it,
resolve Docker container thresholds from cloned defaults plus the override
under the lock, and read the restart-loop and memory-limit settings under
it. Drop getThresholdForMetricFromConfig: nothing has called it since the
v6 split, and it normalized in place.

TestAlertEvaluationLeavesTheLiveConfigToTheLock runs host, guest and Docker
evaluation against GetConfig() and UpdateConfig(GetConfig()) under -race.
With host.go, guest.go, docker.go and metric_runtime.go overlaid by their
origin/main copies it fails: the live override's clear reads 85, and -race
reports every site above.

Stacked on #2692, which made GetConfig return an owned copy.
2026-10-07 17:19:43 +01:00
pulse-triage[bot] 819e5de1b4 Preserve reviewed maintenance with the published Patrol context cleanup
Additively compose exact upstream 5c55197900 imported by the bounded refresh receipt. Preserve every reviewed commit and the existing pending proposal; final-content browser coverage and exact composed-source validation remain required before publication.

Change-source: pulse-maintainer
2026-10-07 17:17:59 +01:00
rcourtman 5c55197900 Remove test-only Patrol context summary helpers (#2674)
Removes two exports from
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`
that only tests still called, and corrects the contract prose that
described the removed Patrol supporting-context surface as live.

## Why

`buildPatrolInvestigationContextSummary` (with its input type) and
`selectPatrolSupportingRecentChanges` lost their last production caller
in 27ba85ddc6 (2026-06-23). That commit removed the Patrol page's
supporting-context block and its `Details` toggle for recent changes,
learned correlations and policy coverage. Since then, only their own
tests and a raw-source guard referenced them.

## Evidence

- A TypeScript-compiler import graph rooted at `src/index.tsx` on
current main shows no reachable importer of either export. `git grep`
finds only the tests and the guard.
- Before/after production `vite build`: byte-identical across all 187
dist files, because Rollup had already tree-shaken the code.
`check-bundle-size.mjs` passes and `.bundlesize.json` is unchanged.
- Browser pass on an embedded build of this tree (demo-mode mock stack,
1440x900 and 390x844). None of these show recent-change, correlation,
policy-coverage or `Details` context UI: Patrol Inbox, Protection,
Activity, Finding options and history, the finding review panel, the run
findings snapshot and the selected run. The attention ("Explain with
Assistant") and run ("Discuss") handoffs from this module still open the
Assistant drawer with their briefings. Receipt:
`frontend-modern/browser-verification/eaf59cf8f93521c4.json`.
- `tsc --noEmit`, eslint and prettier pass. Vitest passes over
`src/features`, `src/utils`, `src/components/AI`,
`src/components/patrol`, `src/stores` and `src/pages`. The only failure
is `agentInstallCommand.test.ts`, which fails on the Mac regardless of
this change. `contract_audit.py`, `registry_audit.py` and
`status_audit.py` all pass.

## Guard

The raw-source guard in `frontendResourceTypeBoundaries.test.ts` pinned
the removed builder. It now does three things:
- It pins the export of
`buildPatrolAssistantFindingHandoffFromUnifiedFinding`, which the Patrol
hook imports.
- It checks the hook's import declaration with one regex, instead of the
symbol and the module path as separate strings. Moving the import to
another module now fails, and a single-line import still passes.
- The `policy-covered resource` copy pin is now a negative pin.

## Contracts

The patrol-intelligence, frontend-primitives, api-contracts and
ai-runtime clauses that described the removed surface now match the
code:
- Patrol renders no recent changes, correlations, posture or summary
card, and reads the AI summary only for overall health.
- There is no `Details` context affordance. One rule that still allowed
it contradicted an existing prohibition.
- The shared change and correlation cards carry no resource-link
default.
- Rules for a returning summary or recent-change normalization are now
conditional.

## Left for other PRs, already in progress

- The equally unreachable Patrol assessment and save-failure Assistant
handoff builders, their prose, and the retired summary-card and
assessment-strip clauses.
- The Patrol refresh path's unread `/api/ai/intelligence/correlations`
fetch, plus the two api-contracts clauses about it and the matching
unified-resources.md prose.
2026-10-07 17:14:56 +01:00
pulse-triage[bot] 81271e4b4c Preserve reviewed monitoring maintenance with current upstream
Compose the exact current upstream shared-clock, last-known guest readings and platform-alert ownership repairs without rewriting the reviewed batch. Source proofs, final-content browser evidence and hosted checks remain separate publication prerequisites.

Change-source: pulse-maintainer
2026-10-07 17:09:16 +01:00
rcourtman 409c5482ee Read controller-member disk history at its written key
A physical disk behind a RAID controller (target such as megaraid,3)
with no usable serial or WWN had its SMART history written under one
key and read under another, so its temperature and SMART charts stayed
empty. The host agent writes under HostSMARTDiskMetricID and Proxmox
under PhysicalDiskMetricID, and both keys already scope the member
(host-pve:sdd@ctrl0/megaraid,3). BuildMetricsTarget passed that source
ID to PhysicalDiskMetaMetricID, which appended the topology again
(host-pve:sdd@ctrl0/megaraid,3:sdd@ctrl0/megaraid,3).

The reader now does what the Proxmox writer does: a fallback that
already names the member is returned unchanged, and only a fallback
without the topology gets it appended. That covers a Proxmox source ID
from before members were scoped and the canonical resource ID a view
falls back to. TrueNAS disks carry no controller target and are
unaffected.

SMART history needs no migration: no SMART writer used the doubled
key, so samples already stored become readable. A linked node's agent
with no SMART reading for a member, such as a standby row, files the
device's I/O under the Proxmox disk's metrics target, which was the
doubled key. That I/O now lands beside the member's SMART history, and
samples already stored under the doubled key are left to age out.

This carries forward the fix from PR 2661, whose branch conflicts with
the disk ID stack it now sits on, and proves it against the real SMART
and I/O writers in the live and rehydrated registries.
2026-10-07 17:01:52 +01:00
rcourtman ad1e34277d Stop alert config saves writing into the live config
SaveAlertConfig normalized the config it was handed in place, and its
callers hand it alerts.Manager.GetConfig(), a shallow copy whose maps
and threshold pointers were the running config's own. Alert activation
and the alert identity migration therefore wrote the live delay map and
threshold pointers outside the manager lock while evaluation read them.
The shallow copy also let UpdateConfig, which normalizes the override
map it is handed in place, rewrite the live overrides while
GET /api/alerts/config encoded another snapshot. A concurrent map read
or iteration plus a write is a fatal Go runtime error, so these paths
could kill Pulse.

AlertConfig.Clone is a typed deep copy. GetConfig returns one, so
callers own what they encode, persist and edit, and SaveAlertConfig
clones before normalizing, because a config just applied with
UpdateConfig is the live config. GetConfig has no per-resource caller
(once per poll cycle, backup check, request or Patrol run); a clone
with 1,000 overrides takes about 0.3 ms. A test fills every field and
fails when a new map, slice or pointer is not copied.

Persistence also stored and loaded an explicit 0 type-level alert delay
("no delay") as the 5-second factory delay, which the noisy-gauge rule
stretches to 300 seconds for memory and temperature, while the manager
keeps 0. The shared map write was what made the running manager agree
with the file; without it, memory would say 0 and disk 5. Persistence
now keeps 0 on save and load like NormalizeTimeThresholds, and only a
missing or negative delay takes the default. Pulse saves never wrote a
0, and the first v6 start re-saves alerts.json, so only a newly sent or
hand-edited 0 changes meaning.

On main, a reproducer taking the activation, migration and GET steps
against live evaluation died with "fatal error: concurrent map
iteration and map write" in 5 of 5 plain runs and reported 16 races
under -race. It passes with this change.
2026-10-07 16:57:57 +01:00
rcourtman 22b8b995ca Read the shared wall clock on Alerts overview cards (#2673)
The overview hook kept its own minute signal and handed its value to
each card as "now". A card that mounted between ticks measured from up
to a minute earlier: an alert raised 70 seconds ago read "this minute",
a reading already past the 10-minute stale cut-off led the card as the
live one, a just-raised alert stayed out of Triggered (24h) until the
next tick, and the stale flip and 24h drop landed up to a minute late.

The card now reads useRelativeTimeNow for its started age and the live
reading's stale cut-off, and the hook reads it for the 24h count, so
they measure from the wall clock and move with the shared 30-second
tick. The minute interval stays only for the delivery diagnoses
refresh, a server read that should not double its rate.
2026-10-07 16:56:58 +01:00
rcourtman 2d6a2937fc Keep platform metric alerts through Proxmox node cleanup (#2672)
`CleanupAlertsForNodes` runs on every Proxmox node poll
(`monitor_pve_storage.go`) and on every mock alert tick. It removes
alerts whose `Node` is not a current Proxmox node or PBS instance, and
keeps the types `shouldPreserveAlertOutsideNodeCleanup` lists.

Kubernetes, TrueNAS and vSphere metric alerts carry a cluster or
platform host as `Node`, and a display label (`TrueNAS Disk`,
`Kubernetes Pod`, `vSphere VM`) in `metadata.resourceType`. The preserve
list only had the bare words `truenas`, `kubernetes`, `k8s` and
`vmware`, so none of these labels matched. In an estate with Proxmox
plus one of those platforms:

- every Proxmox poll deleted these alerts without resolving them;
- their reducer incident stayed firing, so the next unified evaluation
re-created each one and handed it to the alert callback again as a new
alert.

## Change

`shouldPreserveAlertOutsideNodeCleanup` keeps an alert that carries
`metadata.platformType` (set on every unified Kubernetes/TrueNAS/vSphere
metric alert), or whose label maps through `CanonicalAlertResourceType`
to one of those platform types. The label path covers alerts persisted
before `platformType` existed. A guest alert whose Proxmox node left the
estate is still removed.

## Evidence

- Unit probe on main: a TrueNAS disk temperature alert, a Kubernetes pod
CPU alert and a vSphere VM CPU alert were all deleted by
`CleanupAlertsForNodes({"pve1": true})`. Over three
cleanup-then-evaluate cycles the TrueNAS alert went to the alert
callback 3 times; with this change, 0 times and it stays active.
- Private mock stack: TrueNAS disk temperature alerts kept vanishing
between mock alert ticks, while the disks kept their readings in
`/api/resources`. With this change they stay open.

## Tests

`TestCleanupAlertsForNodesKeepsPlatformMetricAlerts` (in
`internal/alerts/alerts_test.go`) raises a TrueNAS disk, a TrueNAS pool,
a Kubernetes pod and a vSphere VM alert, then runs the cleanup three
ways: as raised, with only `platformType` (label unrecognised), and with
only the label (`platformType` removed).
`TestCleanupAlertsForNodesStillRemovesMissingProxmoxNodeAlerts` keeps
the removal of a guest alert on a removed Proxmox node. Each preserve
condition was mutation-checked: removing either one fails the first
test.
2026-10-07 16:56:25 +01:00