diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 964d1cec76..29db86d15b 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -67,7 +67,12 @@ If you run Proxmox VE, the easiest and most “Pulse-native” deployment is the Replace `vX.Y.Z` with the exact release tag you want, then run this on your Proxmox host: ```bash +( +set -e export PULSE_VERSION=vX.Y.Z +pulse_installer_dir="$(mktemp -d)" +trap 'rm -rf "$pulse_installer_dir"' EXIT +cd "$pulse_installer_dir" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh.sshsig" ssh-keygen -Y verify \ @@ -76,7 +81,7 @@ ssh-keygen -Y verify \ -n pulse-install \ -s install.sh.sshsig < install.sh bash install.sh --version "${PULSE_VERSION}" -rm -f install.sh install.sh.sshsig +) ``` > **Note**: The GitHub `install.sh` is the **server** installer. The agent installer is served from your Pulse server at `/install.sh` (see **Settings → Infrastructure → Install on a host**). Do not use the GitHub server installer to install or update `pulse-agent`. @@ -144,7 +149,12 @@ See [KUBERNETES.md](KUBERNETES.md) for ingress and persistence configuration. For Linux servers (VM or bare metal), use the official installer: ```bash +( +set -e export PULSE_VERSION=vX.Y.Z +pulse_installer_dir="$(mktemp -d)" +trap 'rm -rf "$pulse_installer_dir"' EXIT +cd "$pulse_installer_dir" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh.sshsig" ssh-keygen -Y verify \ @@ -153,7 +163,7 @@ ssh-keygen -Y verify \ -n pulse-install \ -s install.sh.sshsig < install.sh sudo bash install.sh --version "${PULSE_VERSION}" -rm -f install.sh install.sh.sshsig +) ``` > **Note**: This installs the Pulse server. Use the `/install.sh` endpoint from **Settings → Infrastructure → Install on a host** for installing or upgrading `pulse-agent` on monitored hosts. @@ -250,7 +260,15 @@ Pulse can update the server runtime to the latest stable version. |----------|---------| | **Docker** | `docker compose pull && docker compose up -d` | | **Kubernetes** | `helm repo update && helm upgrade pulse pulse/pulse -n pulse` | -| **Systemd / Proxmox LXC** | `sudo /bin/update` | +| **Systemd / Proxmox LXC with the Pulse-owned helper** | `sudo /bin/update` | + +Use `/bin/update --version vX.Y.Z` for an exact target only when the helper was +installed by the Pulse server installer. On Proxmox community-scripts +containers, `/bin/update` can belong to a different updater that ignores +`--version`. If the helper is absent or its owner is unknown, use the +[signed server-installer flow](#2-bare-metal--systemd) with `PULSE_VERSION` set +to the exact target tag. The same ownership check applies to rollback. After +the service restarts, verify the installed version with `GET /api/version`. Docker without Compose: `docker restart` keeps the old image running. Run `docker pull rcourtman/pulse:vX.Y.Z`, then `docker stop pulse && docker rm pulse` and re-run your original `docker run` command. diff --git a/docs/UPGRADE_v6.md b/docs/UPGRADE_v6.md index 7000f87f60..8048ffe1ee 100644 --- a/docs/UPGRADE_v6.md +++ b/docs/UPGRADE_v6.md @@ -148,13 +148,23 @@ Preferred path: - **Settings → System → Updates** -If you prefer CLI, use the installed update helper for the target version: +If you prefer CLI, use this command only when `/bin/update` was installed by +the Pulse server installer: ```bash sudo /bin/update --version vX.Y.Z ``` -`/bin/update` is installed by the supported systemd and Proxmox LXC server installer. If your host does not have it yet, follow the signed server-installer flow in [INSTALL.md](INSTALL.md). Agent updates and v5-to-v6 agent upgrades still use the `/install.sh` command generated in **Settings → Infrastructure → Install on a host**; that screen is for both first installs and in-place agent upgrades. +On Proxmox community-scripts containers, `/bin/update` can belong to the +community-scripts updater, which can ignore `--version`. If that is your +installation, the helper is absent, or you cannot confirm its owner, follow the +[signed server-installer flow](INSTALL.md#2-bare-metal--systemd) and set +`PULSE_VERSION` to the exact target tag. This also applies to rollback. Verify +the installed version with `GET /api/version` after the service restarts. + +Agent updates and v5-to-v6 agent upgrades use the `/install.sh` command generated +in **Settings → Infrastructure → Install on a host**. That screen is for both +first installs and in-place agent upgrades. Operator note for builds after `v6.0.0-rc.2`: the historical Pulse update signer was not recovered. Hosts pinned to the `rc.2` trust root should not @@ -273,12 +283,16 @@ older v5 agents do not understand observer configuration. ### Can I keep Pulse v5 stable while I test Pulse v6? Yes. Keep a rollback path available while you evaluate v6. The final release -on the v5 line is 5.1.36, so the stable rollback command is: +on the v5 line is 5.1.36. On installations with the Pulse-owned update helper, +the rollback command is: ```bash sudo /bin/update --version v5.1.36 ``` +The [installer ownership requirements](#systemd-and-proxmox-lxc-installs) above +also apply to this command. + ### Why did my v5 install upgrade itself to v6? Pulse 5.1.29 and later pin the built-in updater to the 5.1.x line and never @@ -286,12 +300,16 @@ offer v6, so upgrading from those versions is always a manual step. Pulse 5.1.28 and older have no such pin: installs with auto-update enabled follow the newest stable GitHub release, which is now v6. If that happened to you, your data and configuration carry over; run through the Post-Upgrade -Checklist above to confirm everything still works. To return to v5, run: +Checklist above to confirm everything still works. To return to v5 with the +Pulse-owned update helper, run: ```bash sudo /bin/update --version v5.1.36 ``` +For community-scripts installations or an unknown helper owner, use the +[installer ownership guidance](#systemd-and-proxmox-lxc-installs) above. + ## Migration Notes (v6) ### Unified Navigation (Bookmarks and Deep Links) diff --git a/docs/release-control/v6/internal/records/resource-payload-static-metadata-2026-08-24.md b/docs/release-control/v6/internal/records/resource-payload-static-metadata-2026-08-24.md index 9f152cc661..ad7b090281 100644 --- a/docs/release-control/v6/internal/records/resource-payload-static-metadata-2026-08-24.md +++ b/docs/release-control/v6/internal/records/resource-payload-static-metadata-2026-08-24.md @@ -85,3 +85,69 @@ resolution is load-bearing client-side while history behind a detail endpoint needs the alert-override/threshold matching reworked onto an on-demand lookup. Platform source payloads (~1.4MB) are live data, not static metadata, and are out of scope for this gap. + +## 2026-09-24 reliability audit measurement + +The gap remains open after the September telemetry coalescing improvements. +An isolated production frontend build on `pulse-dev`, based on +`0abe518219f75512fc3f9ca0fdbd4ce549e30e90` plus the alert-admission and installer +guidance repair, used the 50-node synthetic estate. Random metrics changed +every two seconds. The measured `/api/state` response was 4,015,466 bytes. +This is a REST response measurement, not a measurement of every websocket +frame or proof of the contribution of an individual field. + +The existing browser performance recipe ran on 2026-09-24 from 13:27:23 to +13:29:56 UTC. At 1440 x 900 without CPU throttling, the 30-second idle window +contained seven long tasks totalling 3,531 ms, with a maximum of 1,037 ms. +At 390 x 844 with 4x CPU throttling, seven long tasks totalled 10,104 ms, with +a maximum of 2,748 ms. Host load was 0.76/1.55/2.22 before the run and +1.47/1.55/2.12 afterwards. The recipe exited successfully, but its desktop +top-level navigation locator no longer matched the UI. Those missing steps +are not navigation proof. The idle measurements do not establish causality +between static metadata and all observed stalls. + +A separate settled-server 30.02-second CPU profile contained 3.54 seconds +of CPU samples, or 11.79% of one core. Canonical identity matching accounted +for 880 ms cumulative in that profile. Three 1,000-resource component runs +measured registry rebuilds at 14.7–15.3 ms, full snapshot construction at +9.4–9.5 ms and delta construction at 0.107–0.109 ms. These results neither +reproduce a reported production CPU incident nor qualify browser +responsiveness. The remaining browser stalls require their own trace-led +investigation and cross-client contract proof before this gap can close. + +Reproduction uses the performance rig recipe in +`pulse-dev-infra/docs/perf-rig.md`, with its existing `perf-audit.mjs` pointed +at the isolated synthetic server. Script SHA-256: +`b068bb3f96009418e21c63d249458447c3041212152536925ef65f694b79cde6`. +CPU profile SHA-256: +`49a75bb9ad6323238e32dfdffa5342cbc77b77282f0ddd4687df012b14488e30`. +Raw logs and profiles are retained in the local reliability audit evidence +bundle under `pulse-audit-implementation-20260924/verification/performance-baseline/`. + +### Final reliability-source observation + +The same recipe was repeated after the alert lifecycle, startup reconciliation, +retained-row rendering and stable synthetic identity repairs, using final +backend SHA-256 +`77d17ff9e21ccdef3f5fff9f7b91b841abe3570bf8e04e1eda501c554e3a5889` +and frontend index SHA-256 +`b9f1288bd188c8e549ccd4cdfa7aa75b56d49df3fc0f59de9e11dac90d449172`. +The functional browser was closed before measurement. The run lasted from +16:03:53 to 16:07:21 UTC. Although the preceding one-minute load sample was +1.88, the actual start sample rose to 2.49/2.35/3.93 and the end sample was +4.08/3.42/4.05. It therefore does not qualify as a controlled comparison or +performance improvement proof. + +Desktop idle recorded seven long tasks totalling 5,949 ms, maximum 2,705 ms. +The phone viewport with 4x CPU throttling recorded eight totalling 12,971 ms, +maximum 3,732 ms. The authenticated REST snapshots were 4,003,769 and +4,003,907 bytes respectively, both HTTP 200. The unchanged desktop navigation +locators still missed several routes. The harness exit code was zero, but +those steps do not count as navigation acceptance. Functional route and policy +acceptance was performed separately in the final browser interaction matrix. + +These observations leave this gap open. They do not attribute the change in +long-task cost to the source repairs or prove a particular payload field is +responsible. Stable fixture machine IDs are created at graph construction, +not during each metric refresh. The raw final run and host-load receipts are +retained under `pulse-audit-implementation-20260924/verification/performance-final/`. diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 3747844283..dba8b60f95 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -9786,7 +9786,7 @@ }, { "id": "resource-payload-static-metadata", - "summary": "The client-facing resource stream ships ~3KB of mostly-static metadata per resource (duplicated capabilities blobs with only 10 distinct values across 1,508 resources, canonicalIdentity alias/superseded-id history, aiSafeSummary prose, and policy routing) on every snapshot, REST recovery, and reconnect. At 50-node scale the snapshot is a single 4.73MB websocket frame and browser main-thread cost per delta reaches ~1s even on desktop hardware. Slimming the stream is a wire-format change with consumers beyond frontend-modern (pulse-mobile OTA compatibility, Pro surfaces, AI runtime), so it needs an owned slice with contract updates and cross-client verification.", + "summary": "Capabilities catalog, default-policy omission and alias deduplication reduced the resource payload in August, but canonical identity history remains inline. The September 24 reliability audit measured a 4,015,466-byte /api/state response and 1,037 ms maximum desktop idle long task on the 50-node synthetic estate. A final-source repeat still observed multi-second long tasks, but elevated host load prevents a controlled comparison. This remains an open cross-client payload and browser responsiveness gap requiring trace-led attribution, contract updates and mobile/Pro/runtime verification before closure. See the evidence record for measurements and limits.", "owner": "project-owner", "status": "triaged", "recorded_at": "2026-08-24", diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 8c14451813..17fa804828 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -45,6 +45,17 @@ covers unchanged active evaluations, repeated fired/resolved transitions, JSON checkpoint reload, canonical-backed shells and delayed historical events. `TestIncidentStore_LifecycleRapidRecurrence` covers distinct subsecond starts. +An explicit newer refire reopens the same retained occurrence. The checkpoint +persists its refire timestamp so an older resolution, historical read repair, +or repeated refire cannot override the latest transition. Canonical resource +history carries `alert_started_at` separately from transition time, allowing +`QueryIncidents` to reconstruct one occurrence after fire, resolution and refire +even without a saved incident shell. Legacy history without this metadata keeps +its timestamp-based fallback. `internal/ai/memory/incidents_refire_test.go` pins +checkpoint and stale-replay behaviour, while +`TestMonitorLifecycleRefireReopensRetainedOccurrence` exercises both local and +canonical projections, including reconstruction from canonical history. + This does not migrate existing duplicate records, change bounded retention, make acknowledgement event replay idempotent, or prove aggregate write-byte reductions or recipient delivery. An evicted occurrence can still be recreated diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 8c9ef7805c..549454680e 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -15,6 +15,22 @@ ## Purpose +### Retained occurrence re-fires preserve incident truth + +A reducer re-fire inside the retention window keeps its original occurrence +identity and reopens that incident. It is distinct from replaying the initial +firing. Canonical resource changes carry `alert_started_at` separately from the +transition timestamp, so history reconstruction also retains one occurrence. +Legacy records without that key retain their historical timestamp projection. +Checkpointed re-fire timestamps reject earlier resolution replay and historical +read repair. A later resolution still closes the occurrence, and replaying the +older re-fire cannot reopen it. This does not change alert retention or delivery. + +`TestMonitorLifecycleRefireReopensRetainedOccurrence` covers both history paths, +canonical reconstruction without an incident checkpoint, repeated replay and +historical read repair. `TestIncidentRefireCheckpointPreservesTransitionOrder` +covers checkpoint reload on either side of the final resolution. + ### Automatic acknowledgement lifecycle Cleanup-triggered acknowledgement uses the same canonical retention records and @@ -2543,6 +2559,14 @@ hidden by expected-offline. Every active-alert writer passes through the same operator-state gate, and notification delivery consults the same decision before quiet-hours policy. +The active-store writer reports admission explicitly. Detectors must not add +history, recent-alert state, fired events, rate-limit consumption, or delivery +intent after a rejected write. Lifecycle evaluators report suppression without +an activation transition. Restore and guest-identity migration must not revive +acknowledgement or tracking state for a rejected alert. Repeated observations, +policy removal or expiry, and post-mutation reconciliation are exercised across +provider incidents, metric and canonical lifecycle writers in +`internal/alerts/alert_admission_test.go`. Operator suppression is never converted into a quiet-hours replay, including for recovery notifications carrying stale replay metadata. After a persisted policy mutation, `ReconcileResourceOperatorState` resolves diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 51d91a867c..2567250475 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -556,7 +556,18 @@ as complete. Published exact-version install and rollback guidance must preserve the server and Unified Agent installer boundary. Supported systemd and Proxmox LXC -deployments use the signed `/bin/update --version vX.Y.Z` server helper; +deployments may use `/bin/update --version vX.Y.Z` only when the installed +helper belongs to the Pulse server installer. Community-scripts Proxmox +containers may own a different `/bin/update` that ignores the version argument. +Generated install and rollback sections must each state this ownership boundary +and link the signed server-installer flow from the candidate's versioned docs, +with the exact installation or rollback target, for absent or unverified helpers. +Operators verify the resulting version after restart. +Signed installer recipes run in a fresh temporary directory and stop on any +download or signature-verification failure before executing the installer. +`scripts/tests/test_signed_installer_docs.py` executes both documented recipes +with controlled download and verification failures and checks exact-version +forwarding on success. `/opt/pulse/scripts/install.sh` and release archives' `scripts/install.sh` are Unified Agent installers and must never be presented as server rollback commands. Docker guidance instead pins the target image and recreates the diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index d519583c62..73929f097f 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -623,6 +623,27 @@ that manual-channel consequence must stay visible at desktop and narrow widths. ## Shared Boundaries +### Maintenance schedule copy + +Operator maintenance banners show local absolute start and end timestamps, +including future dates. Past-time relative formatting must not turn a future +expiry into "just now". The banner describes paused attention rather than +promising that rejected alert observations are acknowledged. Active and +future-window mounted regressions pin these user-visible claims. + +### Retained state in bounded platform windows + +`PlatformWindowedRows` and `PlatformWindowedList` keep one keyed renderer owner +while the visible window moves. Items present in both windows retain component +identity, active detail tabs and unsaved edits. Passing a new window must update +the existing renderer rather than instantiate another renderer around it. +Removed items still unmount, and row budgets and spacer behavior are unchanged. +The mounted scroll regression in `PlatformWindowedRows.test.tsx` exercises both +renderers with an edited input retained across overlapping windows. The browser +journey is a narrow Proxmox node Manage form, scrolling to lifecycle Save while +the synthetic estate continues updating, plus the alert timeline and note form. + + ### PBS host history correlation The Backups surface passes its complete deduplicated route inventory to the PBS table. Only PBS servers render as rows; other resources supply correlation inputs. Preserve PBS drawer identity and use the correlated canonical history target. Missing disk utilisation does not gate CPU/memory history. diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index f29b5594e4..86b83c85cd 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -17,6 +17,17 @@ ## Purpose +### Synthetic machine identity survives restart + +Linked mock hosts and Docker hosts derive their machine ID from their stable +fixture host ID. Docker agent IDs are stable as well. +Changing metric samples or rebuilding the graph must not give the same fixture +machine a new canonical agent ID, orphaning saved operator policy. The existing +`TestFixtureIdentityStableAcrossBoots` compares machine IDs per host and the +unified canonical agent IDs, in addition to source IDs and names. This is a +fixture guarantee. Production identity matching must still reject a conflicting +nonempty machine ID rather than transfer policy to a different real machine. + ### Canonical registry publication during accepted ingest — issue #2199 The monitor adapter builds a replacement resource generation away from readers. @@ -3918,6 +3929,18 @@ invent lifecycle state; Alerts owns signal suppression and unified resources owns persistence. `internal/monitoring/monitor_alert_intent_test.go` and the alerts intent-policy proof pin this adapter boundary. +Alert restore precedes resource-store attachment during startup. Once the +adapter attaches the persisted operator-policy resolver, monitoring asks Alerts +to reconcile the restored set and refreshes shared alert state if it changed. +Resource publication repeats reconciliation before evaluation and shared-state +export, because native aliases may only become resolvable after the first +observation populates the canonical registry. Persisted agent policy applies +to those restored native-ID alerts even when the initial registry was empty. +Muted or retired resources cannot retain active alerts merely because policy +was unavailable during restore. The resolution survives a subsequent restart +and preserves unaffected alerts. This ordering is pinned by +`internal/monitoring/monitor_alert_restore_test.go`. + `internal/maintenancesentinel/` is monitoring-owned post-maintenance assurance. Its bounded sweep derives every concrete one-shot or recurring occurrence that ended in the seven-day lookback, de-duplicates on canonical resource plus exact diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 6d27232b23..f22568a4f3 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1421,6 +1421,22 @@ without contention or cross-delivery. ## Current State +### Retained component state in bounded platform windows + +`PlatformWindowedRows` and `PlatformWindowedList` retain one keyed renderer +owner as overlapping visible windows change. Components that remain visible +keep their local state and unsaved edits. Removed items still unmount, and the +existing row budgets and spacers remain bounded. The mounted regressions in +`PlatformWindowedRows.test.tsx` exercise both renderers. The final browser +receipt exercises a narrow Proxmox Manage form through scrolling, saving and +viewport changes while the synthetic estate updates. + +This correctness proof does not close the large-estate performance gap. The +final measurements and host-load limitation remain recorded in +`records/resource-payload-static-metadata-2026-08-24.md`. They do not establish +a controlled performance improvement or satisfy the open SLO qualification. + + ### Large API responses negotiate gzip without corrupting edge cases The main listener compresses eligible JSON and frontend text assets for @@ -3139,6 +3155,14 @@ resource results and defensive-copy isolation, while `internal/api/alerting/alerts_test.go` proves that mock transport does not need the production incident store. +Synthetic linked-host machine identities and Docker machine/agent identities +are derived from stable fixture host identity when constructing the graph. +Rebuilding the same estate therefore preserves canonical agent IDs and the +resource policy attached to them. `internal/mock/generator_test.go` verifies +nonempty machine and agent IDs and canonical identity across fixture rebuilds. +This changes fixture generation only, without relaxing production identity +resolution or adding work to the recurring update path. + ### Update evidence reuses the bounded node observation Package evidence adds only one bounded status, one bounded reason, and the diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 12fcc8e950..bda171ca5d 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2519,6 +2519,7 @@ "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ + "internal/alerts/alert_admission_test.go", "internal/alerts/canonical_stateful_test.go", "internal/alerts/incident_synthesis_test.go", "internal/alerts/intent_policy_test.go", @@ -2754,6 +2755,7 @@ "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ + "internal/alerts/alert_admission_test.go", "internal/alerts/alerts_test.go", "internal/alerts/callback_config_coverage_test.go", "internal/alerts/canonical_override_migration_test.go", @@ -7188,6 +7190,7 @@ "frontend-modern/src/stores/__tests__/websocket-unified.test.ts", "frontend-modern/src/utils/__tests__/resourceStateAdapters.test.ts", "internal/mock/alert_incidents_test.go", + "internal/mock/generator_test.go", "internal/mock/integration_coverage_test.go", "internal/mock/platform_fixtures_test.go" ] @@ -8723,6 +8726,7 @@ "exact_files": [ "internal/ai/tools/filesystem_evidence_test.go", "internal/monitoring/docker_filesystem_evidence_test.go", + "internal/monitoring/monitor_alert_handling_test.go", "internal/unifiedresources/action_request_identity_test.go", "internal/unifiedresources/action_result_v2_test.go", "internal/unifiedresources/availability_link_test.go", diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 5046b9c356..271ef9478f 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -2544,6 +2544,17 @@ persists recurrence JSON and scope in additive columns while legacy one-shot rows retain their existing meaning. Scheduling, updating, changing scope, or clearing either form remains an atomic operator-state plus resource-timeline lifecycle write. + +The operator-state banner projects these authoritative occurrence boundaries +as local absolute start and end timestamps, including scheduled future dates. +An active maintenance window describes paused attention rather than claiming +that rejected observations have been acknowledged. Persisted policy remains +keyed by canonical resource identity. Monitoring reconciles restored native +alert aliases after registry publication, before evaluation or shared-state +publication. Mounted operator-state regressions cover active and scheduled +copy, while the monitoring restore regression covers SQLite policy and native +aliases before the first observation. + `NormalizeResourceOperatorState` trims whitespace, de-duplicates capability names, guarantees a non-nil empty capability list, and lower-cases the criticality value before persistence. SQLite reads must apply that @@ -4411,6 +4422,15 @@ projection for notes, analyses, command breadcrumbs, runbooks, and other operator-facing incident memory. Agents must not model the same durable backend fact in both places as competing primary histories. +Alert lifecycle resource changes record a nonzero occurrence start in +`MetadataAlertStartedAt` (`alert_started_at`), separately from the transition +timestamp. A refire retains this start, so the canonical incident query can +reconstruct the original occurrence without an incident-memory checkpoint. +Legacy changes without the metadata retain timestamp-based projection. +`TestMonitorLifecycleRefireReopensRetainedOccurrence` in +`internal/monitoring/monitor_alert_handling_test.go` proves the metadata bridge, +ordered lifecycle history and canonical-only reconstruction together. + The unified resource core is strong and canonical, but monitoring and some frontend/API consumers are still being tightened around it. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 88e568401c..3cfaa62c89 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,37 +1,55 @@ { "version": 1, - "base_sha": "3b1b43293e21d06d1286124f96f78b1247443dea", - "verified_at": "2026-09-24T09:02:03Z", + "base_sha": "0abe518219f75512fc3f9ca0fdbd4ce549e30e90", + "verified_at": "2026-09-24T16:05:19Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Settings/BrandingSettingsCard.tsx" + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx", + "frontend-modern/src/features/platformPage/PlatformWindowedList.tsx", + "frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx" ], "content_sha256": { - "frontend-modern/src/components/Settings/BrandingSettingsCard.tsx": "6516a05aff71b1d96660f81189b6df8bceccf7e94d15d6c3213556e794f2ae40" + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "5c47a4d5d5ff12b0f6cf0e495bd02ebdadccd81135ad1ff91f9f13e90b5a81af", + "frontend-modern/src/features/platformPage/PlatformWindowedList.tsx": "1842c634054e46e24d965a063719065ca1156b3f061d594c878a8866d5b33a3e", + "frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx": "6f5dbc611dfbb00bc2d770fcf06b8dbf329ebd76dc022bb63191480a221cb457" }, "routes": [ - "/settings/system-general (Application branding card, Header preview)" + "/proxmox/overview", + "/alerts", + "/docs/INSTALL", + "/docs/UPGRADE_v6" ], "viewports": [ { - "width": 1280, + "width": 1440, "height": 900 }, { "width": 390, "height": 844 + }, + { + "width": 900, + "height": 900 } ], "states": [ - "No custom logo and no custom name: the preview falls back to the default Pulse lockup", - "Light colour scheme at 1280x900 and 390x844", - "Dark colour scheme at 1280x900" + "Expanded Disaster Recovery D node with Manage selected, unsaved and saved lifecycle selection, and disabled Saving controls.", + "Future maintenance scheduled for 25 September with actual local start/end dates, edited expiry, cancelled window, and active maintenance with an actual future deadline.", + "Mute, retired and active policy states, persisted maintenance and mute after a graceful same-data restart, and stable canonical resource identity.", + "Active and absent node connectivity alert, open retained incident with ordered firing, resolution and re-fire events, expanded Evidence details, and empty disabled Save Note.", + "Empty filtered History, persisted search after reload, cleared search results and intermediate-width filter controls.", + "Signed installer code and helper-ownership guidance, with internal code scrolling and narrow document layout." ], "interactions": [ - "Open Settings > General with the white_label capability present; assert the Header preview renders exactly one svg.pulse-brand-logo (blue disc, white ring, white centre dot) and no \"\u25cf\" text glyph", - "Compare the preview mark with the page header lockup: both render PulseBrandMark at 20px", - "Check the page does not scroll horizontally at 390px wide" + "Show all 50 nodes, expand Disaster Recovery D, select Manage, change lifecycle, scroll to Save, save, return Active and save. Manage and unsaved selection survive the retained-row window shift and viewport changes.", + "Create resource-only future maintenance, edit its end, cancel it, then create a one-hour active window. Inspect actual banner pixels at phone and desktop widths.", + "Save Mute all attention with active maintenance, reload, gracefully restart the same final binary with unchanged data, and reload. Confirm identical canonical agent identity and policy, with the node connectivity alert absent.", + "Cancel maintenance while keeping mute, verify the alert stays absent, then restore normal monitoring. Mute and resume the active occurrence once more and verify one open incident with its original start and ordered resolve/refire events.", + "Open Monitoring menu, close with Escape and verify focus returns, reopen and dismiss with outside click. Inspect menu placement and clipping at phone width.", + "Open Timeline and Evidence details, inspect occurrence and observation timestamps, verify empty Save Note is disabled without submitting a note.", + "Filter History to no results, reload to verify persisted search, then clear it. Follow signed-installer and ownership links to their headings and inspect code and narrow layout." ], - "command": "node verify.mjs (Playwright against a private mock-mode backend on :7681 and Vite on :5199)", - "notes": "Chromium via Playwright 1.56.1 from repos/pulse/node_modules. Real app and backend in PULSE_MOCK_MODE with ALLOW_ADMIN_BYPASS; the only stub is /api/license/runtime-capabilities with white_label added, because the mock install has no white-label entitlement and the card is gated behind it. Before the fix the same preview drew a blue circle containing a \"\u25cf\" text character instead of the Pulse brand mark, as seen in a provider-hosted client workspace on the walkthrough lab. Console errors were only the mock environment's failing update check." + "command": "Playwright browser inspection against an isolated synthetic server on pulse-dev:7701 through a loopback SSH tunnel.", + "notes": "Final backend SHA-256 77d17ff9e21ccdef3f5fff9f7b91b841abe3570bf8e04e1eda501c554e3a5889. Frontend index SHA-256 b9f1288bd188c8e549ccd4cdfa7aa75b56d49df3fc0f59de9e11dac90d449172. Actual pixels and DOM state were inspected. Repeated suppressed observations, expiry, delayed lifecycle replay and native-alias startup ordering are covered by failing-baseline regression tests and final race checks. The synthetic provider does not qualify production notification destinations or installed upgrade/rollback behavior." } diff --git a/frontend-modern/public/docs/INSTALL.md b/frontend-modern/public/docs/INSTALL.md index 964d1cec76..29db86d15b 100644 --- a/frontend-modern/public/docs/INSTALL.md +++ b/frontend-modern/public/docs/INSTALL.md @@ -67,7 +67,12 @@ If you run Proxmox VE, the easiest and most “Pulse-native” deployment is the Replace `vX.Y.Z` with the exact release tag you want, then run this on your Proxmox host: ```bash +( +set -e export PULSE_VERSION=vX.Y.Z +pulse_installer_dir="$(mktemp -d)" +trap 'rm -rf "$pulse_installer_dir"' EXIT +cd "$pulse_installer_dir" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh.sshsig" ssh-keygen -Y verify \ @@ -76,7 +81,7 @@ ssh-keygen -Y verify \ -n pulse-install \ -s install.sh.sshsig < install.sh bash install.sh --version "${PULSE_VERSION}" -rm -f install.sh install.sh.sshsig +) ``` > **Note**: The GitHub `install.sh` is the **server** installer. The agent installer is served from your Pulse server at `/install.sh` (see **Settings → Infrastructure → Install on a host**). Do not use the GitHub server installer to install or update `pulse-agent`. @@ -144,7 +149,12 @@ See [KUBERNETES.md](KUBERNETES.md) for ingress and persistence configuration. For Linux servers (VM or bare metal), use the official installer: ```bash +( +set -e export PULSE_VERSION=vX.Y.Z +pulse_installer_dir="$(mktemp -d)" +trap 'rm -rf "$pulse_installer_dir"' EXIT +cd "$pulse_installer_dir" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh.sshsig" ssh-keygen -Y verify \ @@ -153,7 +163,7 @@ ssh-keygen -Y verify \ -n pulse-install \ -s install.sh.sshsig < install.sh sudo bash install.sh --version "${PULSE_VERSION}" -rm -f install.sh install.sh.sshsig +) ``` > **Note**: This installs the Pulse server. Use the `/install.sh` endpoint from **Settings → Infrastructure → Install on a host** for installing or upgrading `pulse-agent` on monitored hosts. @@ -250,7 +260,15 @@ Pulse can update the server runtime to the latest stable version. |----------|---------| | **Docker** | `docker compose pull && docker compose up -d` | | **Kubernetes** | `helm repo update && helm upgrade pulse pulse/pulse -n pulse` | -| **Systemd / Proxmox LXC** | `sudo /bin/update` | +| **Systemd / Proxmox LXC with the Pulse-owned helper** | `sudo /bin/update` | + +Use `/bin/update --version vX.Y.Z` for an exact target only when the helper was +installed by the Pulse server installer. On Proxmox community-scripts +containers, `/bin/update` can belong to a different updater that ignores +`--version`. If the helper is absent or its owner is unknown, use the +[signed server-installer flow](#2-bare-metal--systemd) with `PULSE_VERSION` set +to the exact target tag. The same ownership check applies to rollback. After +the service restarts, verify the installed version with `GET /api/version`. Docker without Compose: `docker restart` keeps the old image running. Run `docker pull rcourtman/pulse:vX.Y.Z`, then `docker stop pulse && docker rm pulse` and re-run your original `docker run` command. diff --git a/frontend-modern/public/docs/UPGRADE_v6.md b/frontend-modern/public/docs/UPGRADE_v6.md index 7000f87f60..8048ffe1ee 100644 --- a/frontend-modern/public/docs/UPGRADE_v6.md +++ b/frontend-modern/public/docs/UPGRADE_v6.md @@ -148,13 +148,23 @@ Preferred path: - **Settings → System → Updates** -If you prefer CLI, use the installed update helper for the target version: +If you prefer CLI, use this command only when `/bin/update` was installed by +the Pulse server installer: ```bash sudo /bin/update --version vX.Y.Z ``` -`/bin/update` is installed by the supported systemd and Proxmox LXC server installer. If your host does not have it yet, follow the signed server-installer flow in [INSTALL.md](INSTALL.md). Agent updates and v5-to-v6 agent upgrades still use the `/install.sh` command generated in **Settings → Infrastructure → Install on a host**; that screen is for both first installs and in-place agent upgrades. +On Proxmox community-scripts containers, `/bin/update` can belong to the +community-scripts updater, which can ignore `--version`. If that is your +installation, the helper is absent, or you cannot confirm its owner, follow the +[signed server-installer flow](INSTALL.md#2-bare-metal--systemd) and set +`PULSE_VERSION` to the exact target tag. This also applies to rollback. Verify +the installed version with `GET /api/version` after the service restarts. + +Agent updates and v5-to-v6 agent upgrades use the `/install.sh` command generated +in **Settings → Infrastructure → Install on a host**. That screen is for both +first installs and in-place agent upgrades. Operator note for builds after `v6.0.0-rc.2`: the historical Pulse update signer was not recovered. Hosts pinned to the `rc.2` trust root should not @@ -273,12 +283,16 @@ older v5 agents do not understand observer configuration. ### Can I keep Pulse v5 stable while I test Pulse v6? Yes. Keep a rollback path available while you evaluate v6. The final release -on the v5 line is 5.1.36, so the stable rollback command is: +on the v5 line is 5.1.36. On installations with the Pulse-owned update helper, +the rollback command is: ```bash sudo /bin/update --version v5.1.36 ``` +The [installer ownership requirements](#systemd-and-proxmox-lxc-installs) above +also apply to this command. + ### Why did my v5 install upgrade itself to v6? Pulse 5.1.29 and later pin the built-in updater to the 5.1.x line and never @@ -286,12 +300,16 @@ offer v6, so upgrading from those versions is always a manual step. Pulse 5.1.28 and older have no such pin: installs with auto-update enabled follow the newest stable GitHub release, which is now v6. If that happened to you, your data and configuration carry over; run through the Post-Upgrade -Checklist above to confirm everything still works. To return to v5, run: +Checklist above to confirm everything still works. To return to v5 with the +Pulse-owned update helper, run: ```bash sudo /bin/update --version v5.1.36 ``` +For community-scripts installations or an unknown helper owner, use the +[installer ownership guidance](#systemd-and-proxmox-lxc-installs) above. + ## Migration Notes (v6) ### Unified Navigation (Bookmarks and Deep Links) diff --git a/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx b/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx index 56f6feb0ac..091c5d1dd9 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx @@ -27,7 +27,7 @@ import { describeResourceInventoryOwnership } from '@/utils/resourceMonitoringPo * - Lock the resource against automated remediation (action broker * refuses dispatch with resource_remediation_locked:) * - Schedule a maintenance window during which all findings on the - * resource get auto-acknowledged with cause=maintenance_window + * resource have their alert and Patrol attention paused * * The section stays compact and out of the way until the operator has * something to say about the resource — fresh-install resources see a @@ -576,18 +576,16 @@ export const ResourceOperatorStateSection: Component
- Maintenance window active. Findings raised on this - resource + Maintenance window active. Attention on this resource {' '} and its descendants {' '} - are auto-acknowledged until{' '} - {formatRelativeTime( + is paused until{' '} + {new Date( activeMaintenanceWindow()!.maintenanceActiveEndAt ?? activeMaintenanceWindow()!.maintenanceEndAt!, - { compact: true }, - )} + ).toLocaleString()} . Reason: {activeMaintenanceWindow()!.maintenanceReason} @@ -597,11 +595,9 @@ export const ResourceOperatorStateSection: Component
- Maintenance window scheduled. Auto-acknowledgement will - start{' '} - {formatRelativeTime(scheduledMaintenanceWindow()!.maintenanceStartAt!, { compact: true })}{' '} - and end{' '} - {formatRelativeTime(scheduledMaintenanceWindow()!.maintenanceEndAt!, { compact: true })}. + Maintenance window scheduled. Attention will be paused + from {new Date(scheduledMaintenanceWindow()!.maintenanceStartAt!).toLocaleString()} until{' '} + {new Date(scheduledMaintenanceWindow()!.maintenanceEndAt!).toLocaleString()}. Reason: {scheduledMaintenanceWindow()!.maintenanceReason} diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.render.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.render.test.tsx index b482b0c195..6b74f58d94 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.render.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.render.test.tsx @@ -92,3 +92,27 @@ describe('ResourceOperatorStateSection render with capabilityNames: null', () => expect(screen.queryByText('Allowed actions')).toBeNull(); }); }); + +describe('maintenance schedule timestamps', () => { + it.each(['active', 'scheduled'] as const)( + 'shows absolute future times for a %s window', + async (kind) => { + const start = new Date(Date.now() + (kind === 'active' ? -60_000 : 3_600_000)).toISOString(); + const end = new Date(Date.now() + 7_200_000).toISOString(); + apiClientMock.apiFetchJSON.mockResolvedValue({ + canonicalId: `maintenance-${kind}`, + maintenanceStartAt: start, + maintenanceEndAt: end, + }); + render(() => ); + const badge = await screen.findByText( + kind === 'active' ? 'Maintenance window active.' : 'Maintenance window scheduled.', + ); + expect(badge.parentElement).toHaveTextContent(new Date(end).toLocaleString()); + if (kind === 'scheduled') + expect(badge.parentElement).toHaveTextContent(new Date(start).toLocaleString()); + expect(badge.parentElement).not.toHaveTextContent('just now'); + expect(badge.parentElement).toHaveTextContent(/attention.*paused/i); + }, + ); +}); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts index 39e34c8ff2..37ee1f060f 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts @@ -157,8 +157,6 @@ describe('ResourceOperatorStateSection', () => { // nothing once it ends. expect(sectionSource).toContain('scheduledMaintenanceWindow'); expect(sectionSource).toContain('Maintenance window scheduled.'); - expect(sectionSource).toContain('Auto-acknowledgement will'); - expect(sectionSource).toContain("start{' '}"); }); it('exposes Edit window and Cancel window controls when a window exists', () => { diff --git a/frontend-modern/src/features/platformPage/PlatformWindowedList.tsx b/frontend-modern/src/features/platformPage/PlatformWindowedList.tsx index 9aa3897d25..59e0181cdd 100644 --- a/frontend-modern/src/features/platformPage/PlatformWindowedList.tsx +++ b/frontend-modern/src/features/platformPage/PlatformWindowedList.tsx @@ -18,19 +18,23 @@ export function PlatformWindowedList(props: PlatformWindowedListProps ( - {(item, index) => props.children(item, () => globalOffset + index())} + // Pass reactive inputs through to one For owner so overlapping windows + // preserve the mounted item and any local interaction state. + const renderItems = (items: Accessor, globalOffset: Accessor) => ( + + {(item, index) => props.children(item, () => globalOffset() + index())} + ); return ( - + 0)}>