mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
b4cd2f5c9aebce69aa0511bf3adc5b2cd4e512db
6895 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b4cd2f5c9a |
Stop pin succession erasing an operator split
A source the operator split from a sibling that reports the same machine
(unlink or report-merge) lands on its own source-specific ID and pins the
shared machine key there. The store keeps one pin per key, so with the
sibling absent the source mints the sibling's ID and pin succession read
its pin as that ID's earlier era. Applying the pair rewrote the exclusion
(sibling, source) into (sibling, sibling), and the sibling's return merged
the two: the agent count across {both, source only, both} rebuilds was
2, 1, 1 instead of 2, 1, 2 (37 of 40 trials of the unforced sequence).
PersistIdentityPins now refuses a succession whose retired ID the operator
excluded from its successor, and drops the successions of one batch that
would retire two IDs the operator split from each other onto one
successor. A split that pairs the retired ID with a third resource still
follows the succession. Both checks read the registry's decisions, so a
rebuild that could not read them guards the split from the exclusions it
carried.
While the sibling is absent the split source is listed under the sibling's
ID, so a remediation lock set on its candidate ID does not cover it until
the sibling returns; before, the lock was carried onto the sibling's ID
and the split erased. The unified-resources contract records that boundary
and the limits of the check.
|
||
|
|
9aa7ce2c92 |
Build the mock linked view at the rebuild's link boundary (#2799)
## Why PR 2793 made the monitor's rebuild judge chains of operator links once, over the assembled estate: it holds the link pass back until the snapshot and every record source except availability are ingested. The mock-mode fixture build (`FixtureGraph.unifiedResourceSnapshot`) still ran a link pass per ingest call, which 2793's contract named as a gap. For a chain whose answer depends on the order its members' sources arrive in, mock mode could settle on a different row than the live rebuild. A Proxmox storage, a TrueNAS VM and a vSphere VM linked in a cycle is the test estate: mock mode kept the vSphere VM where the rebuild keeps the TrueNAS VM. PR 2732's invariant is that the mock staged build projects like live. ## What - `ResourceRegistry.DeferManualLinks()` and `ApplyDeferredManualLinks(thresholds)` are exported from `manual_links.go`. Deferral is opt-in, and a registry with no links defers nothing, so the shared memoized (unlinked) snapshot is built exactly as before. The apply takes the stale thresholds the ingests were given (nil takes the registry's own), like the `Ingest*` calls. - The fixture build defers, ingests the snapshot and the TrueNAS and vSphere records, runs the held-back pass once, then ingests availability, so a check linked to a folded resource still projects onto its primary. - `TestFixtureGraphAppliesManualLinksAtEachIngestStage` is renamed `TestFixtureGraphAppliesManualLinksAtTheRebuildsBoundary` (the old name would now state the opposite). It keeps the availability projection as a subtest and adds the cycle on real fixture data, compared against an actual `MonitorAdapter` rebuild. A per-call control in the test fails loudly if a fixture change stops separating the two. - Two unit tests in `registry_merge_policy_test.go` pin the exported pair: the snapshot and record ingests wait, the pass runs once, and it judges freshness by the thresholds it is given. - Contracts: the mock-mode section of unified-resources.md, the clause 2793 wrote about the mock build, the monitoring.md test reference, and a performance-and-scalability.md note. A stale line is dropped there too: a check that names a folded resource by canonical ID follows the fold to the primary (`TestAvailabilityLinkToALinkFoldedResourceFollowsItsPrimary` pins it). ## Evidence On pulse-dev against 2793's head: - Negative control: with `internal/mock/platform_fixtures.go` back at the base, the cycle subtest fails (`mock build kept [vm vm-aceb... [proxmox truenas vmware]], want ... [vm vm-7f6e... ...]`), the availability subtest and the shared-snapshot test still pass; restored, all pass. - Second control: ignoring the thresholds argument fails `TestRegistryDeferredLinkPassJudgesFreshnessByTheCallersThresholds` (a 3-minute-old vSphere disk reading survives under 5-minute thresholds and loses to the fresh agent under the default). - Whole `internal/mock` and `internal/unifiedresources` packages pass, plus the `internal/monitoring` mock-view tests. Cost, measured with a scratch timing test on the default fixture estate: the unlinked memoized build is unchanged (about 52 ms and 65 MB with or without the change). A linked build, which only runs when the fixture data version or the link list changes, pays one extra refresh round after the deferred pass: about 3 ms and 5 MB more, the same round the rebuild runs. ## Not in this change The mock view's cache key (`sameManualLinks`, `internal/monitoring/monitor.go`) ignores a link's `CreatedAt`, which 2793's chain precedence now reads, so a timestamp-only replacement of a link would keep serving the cached row. That file is held by other work; it is a separate follow-up. |
||
|
|
02e4ce2bcc |
Require page-owned state on WorkloadsSurface (#2802)
`WorkloadsSurface` built its own workloads state when no `state` prop
arrived, and its props type extended every `useWorkloadsState` option.
Both production mounts (the Proxmox and vSphere overviews) already build
that state themselves, since it also drives their one `WorkloadsFilter`,
and pass it in. So the fallback only ran in tests, and the mounts passed
`vms`, `containers`, `nodes`, `useWorkloads` (and on Proxmox the
app-container exclusion) that the surface ignored once it had a state.
### What changed
- The surface requires `state` and accepts only `forcedPlatform`,
`forcedViewMode`, the empty-state copy and `tableTitle`. The hook's
options type is now `WorkloadsStateOptions`, so it no longer shares a
name with the surface's props.
- Both pages drop the ignored mount props. The hook-call options
(`excludedWorkloadTypes`, `showNestedExcludedWorkloads`, ...) stay where
they act.
- `WorkloadsSurface.k8s` and `.performance.contract` tests build state
through a `PageOwnedWorkloadsSurface` harness, as the pages do.
Stub-state renders just lose the dead props.
- New pins: the Proxmox page contract test asserts the exact props its
mount passes; the overview guardrails check that each surface repeats
the platform and view scope its page gave the state, and that the
surface imports only the state's type (AST, so formatting and aliasing
cannot get past it).
- `ProxmoxBackupsTable.test.tsx` loses its two pins on the removed JSX
attributes; the hook-call pins that protect backup coverage stay.
- Contract prose in performance-and-scalability, storage-recovery,
unified-resources and frontend-primitives.
### Bundle budget
Once the surface imports only the state's type, Rollup names the chunk
the Proxmox and vSphere pages share after its last module (`rollup`
`getFallbackChunkName` takes `orderedModules[last]`), which is now
`useWorkloadsState`, and the bundle check reported the tracked
`WorkloadsSurface` key as `MISSING`. It is the same code at the same
size, so `.bundlesize.json` renames the key and leaves the baseline and
limit alone. I did not pin the name through `manualChunks`: that would
change chunk composition for a naming concern.
### Evidence (pulse-dev, origin/main
|
||
|
|
93aafe9e1e |
Drop the unread postures response meta from the frontend (#2801)
The Proxmox Backups tab reads the rows and the policy of `GET /api/recovery/postures` and nothing else, but the frontend response type still declared the endpoint's pagination `meta`, and `useProtectionPostures` wrote it twice: an empty one in the initial value, and one it invented after joining its 200-ID batches (`page: 1`, the batch size as `limit`, the row count as `total`, the request count as `totalPages`). This removes it, and `RecoveryResponseMeta` with it, the last response type to carry it now that #2787 dropped the points response's. The backend and the wire shape are unchanged. ## What changed - `types/recovery.ts`: `meta` leaves `ProtectionPosturesResponse`; `RecoveryResponseMeta` is deleted. - `useProtectionPostures.ts`: the initial value and the joined response are `data` and `policy` only. - `ProxmoxBackupsTable.test.tsx`: the four postures fixtures stop sending `meta` (the `meta` blocks of the unrelated `/api/backups/*` payloads stay). - `useProtectionPostures.test.ts`: a type-level pin that the response is `data | policy`, and a runtime test that joins a 201-ID fleet into two requests and checks the batch sizes, the rows (in the backend's order, which the mock deliberately makes non-sorted), the policy and the joined response's keys. Nothing covered the join or the policy flow before: dropping `policy` from the joined response passed every existing test. - `storage-recovery.md`: one paragraph next to the points-decode rule from #2787. `frontend-modern/browser-tests/composed-backups.cjs` still has a `meta` in a route mock of the postures response. Nothing references that script and the mock mirrors the unchanged wire, so I left it. ## Evidence - **No readers.** A whole-program scan with the TypeScript checker over `src` and its tests (object-literal keys resolved through the contextual type, destructuring, element access, indexed-access types, whole-object uses) found zero reads of `ProtectionPosturesResponse.meta` and of the four `RecoveryResponseMeta` members, only the two writers. Controls: `data` and `policy` show their reads, and a planted file with eight reader forms (property access, destructuring, chained access, `['meta']`, a `RecoveryResponseMeta` member read, spread, `Object.keys`, `T['meta']`) was detected in every form. The whole-object uses are the declaration and `initialValue`, and `createNonSuspendingQuery` never inspects the value's shape. Nothing in `tests/integration` reads it either. After the change the scan finds no declaration to report and `tsc` is clean. - **`limit` and `totalPages`.** Nothing reads them; the URL's `limit=200` comes from `MAX_PROTECTION_POSTURE_BATCH_SIZE`, which stays. - **Controls on the new tests.** Re-adding `meta` (optional or required) to the type fails `tsc` on the pin, and with the pin deleted nothing else fails, so the pin is the guard. Each of these single flaws in the hook fails the new runtime test: dropping the rows, dropping the policy, keeping only the first batch, joining batches in reverse, re-sorting the rows, and spreading a batch's wire `meta` back into the joined response (type-clean, which is why the test also asserts the joined keys at runtime). The dropped rows also fail six table tests. - **Tests.** tsc, eslint, prettier and the canonical platform audit are clean; 33 files and 706 tests pass over the hook, the Proxmox features, the recovery transport and the platform-table guardrails. A wide run (`features`, `hooks`, `utils`, `api`, `types`, `__tests__`, `components/shared`, `Workloads`, `Storage`) on the #2787 head plus this change passes 946 of 948 files; the failing file is `agentInstallCommand.test.ts`, whose pty helper raises a `termios` ioctl error on this Mac and which imports nothing this change touches. - **Browser, before/after.** Production builds of this commit's parent (`4471dcaaba`) and of this change, each behind `vite preview` and a private mock backend (sized so the tab's postures request really splits into two batches, 200 and 18 IDs), driven by one headless Chromium script at 1440x900 and 390x844, two runs per build interleaved. Both views (`/proxmox/backups/date` and `/coverage`) give the same 221 targets, the same 109/61/38/10/0/3 posture counts and footnote, the same 144 rows (40 at phone width), the same chips and byte-identical digit-masked page text; the same two postures requests open and poll; every response body still has `data`, `meta` and `policy`; with the endpoint stubbed to a 500 both builds show the "Protection posture is unavailable" notice. 38 measurements, one value each across the four runs, no failing responses, no console errors. - **Bundle.** 189 files each side; one differs in code, `Proxmox-*.js` (276,802 to 276,696 bytes), and only by the removed `meta` statements. Bundle check passes with `.bundlesize.json` unchanged (Proxmox chunk 77.78 to 77.74 kB gzip, total JS 1365.65 to 1365.57 kB). |
||
|
|
3465804dd1 |
Stop mock-mode callers that read identity rebuilding the estate view (#2798)
Follow-up to #2778. Stacks cleanly on main; the contract prose cross-references the wiring change in #2796, so land that first if both are open. ## What was wrong In mock mode the unified view is keyed on the fixture data version, which advances on every metric tick, and building it for the default 1,776-resource estate costs about half a second of CPU (many seconds on a starved process). #2778 moved the metrics-target lookup onto the fixture structure revision. The other callers that read only what the estate lists and how it connects still rebuilt the whole view after each tick: at startup they made 42 view requests, all misses (the Docker-alert prune 18, the connection snapshots 13, the host-agent evaluation 8, the chart prewarm 2, the metrics-target lookup 1), and with no client connected the monitor tick alone rebuilt the estate about every nine seconds. ## What changes - Callers that read identity and topology share the view built for the current structure revision and operator links (`currentStructureUnifiedStateView`, `GetUnifiedStructureReadState`, `currentModeStructureReadState`, in `internal/monitoring/unified_structure_view.go`; `mockMetricsTarget` uses the same cache reader): the Docker-alert prune, the workload chart prewarm, the demo Patrol operator-state resolver, and the host-agent evaluation. `evaluateHostAgents`, which runs on every monitor tick, now lists nodes only when an offline host names a linked node. - The connection-degraded alert feed keeps only platform rows (`snapshotConnectionsForAlerts` drops every agent row, and a PBS row's reported node name only becomes a host alias that its snapshot does not carry). It now builds the aggregator inputs with `platformRowsOnly`, so it no longer reads the hosts, their desired-config fingerprints or the PBS instances. The connections endpoint, diagnostics and runtime inventory still read them. - Deliberately not moved: - The mock storage chart (`resolveMockStorageTotal`): a linked pool takes its capacity from whichever source was seen last, which a tick can move without a structure change. It stays on the data-version view, read once per request instead of once per uncached pool. - `linkedHostForNode`, `getHostAgentTemperatureForNode` and `carriedTemperatureOutlivesAgentLease` (only reachable with `PULSE_MOCK_KEEP_REAL_POLLING`): they read a host's status, disks, sensors and report lease, which a tick moves. Hoisting them would need the read state threaded through the node poller, and they run after startup, so they stay as they were. Real mode is unchanged. ## Measured (pulse-dev, probe builds, one pinned core, GOMAXPROCS=1, host load 30 to 110, single runs) | | #2796 alone | with this change | |---|---|---| | estate view builds before the listener answers | 2 | 1 | | estate builds, no client connected, 299 s of life | 33 | 2 (both at startup) | | process CPU over a 180 s steady window | 12.3 s | 3.3 s | A separate run of the pre-#2796 build used 9.0 s over that window, so read 12.3 against 3.3 as the comparison rather than as exact percentages. The public demo host runs a mock estate, so the steady-state saving is the one that matters there. ## Verification - New tests (in registered proof files): no consumer of the structure view rebuilds the view after a fixture tick (Docker prune, structure node listing, workload prewarm, host-agent evaluation including an offline host that names a linked node); the data-version view still rebuilds; the structure view follows a structural fixture change and an operator link; lazy correlation lists nodes only for a linked host; a storage chart reads the estate only when it is not cached; the alert feed does not rebuild the view, its platform rows equal the full aggregator's, and configured PVE/PBS/PMG rows survive without the hosts; the Patrol resolver does not rebuild the view. - Negative controls (each fails the named test): prune, prewarm, eager node listing, structure node listing, alert feed and Patrol resolver each moved back to the data-version view, and a storage chart that reads the estate before checking its cache. - `go vet` and the targeted tests on pulse-dev. ## Known limits - The structure view can be several ticks old, so a caller on it must not read status, metric values, timestamps, sensors or anything a link merges by freshness; the contract says so. The fixture sets pod node names (which feed the platform aliases a Patrol reference can resolve through) when the estate is built, not on a tick. - `pbsInstanceFromReadStateView` never copies `NodeName`, so `pbsReportedNodeNamesByInstance` appears to feed empty host aliases on the connections endpoint. That is a separate bug, not changed here. - The batch sharing of the storage chart's read (`sync.OnceValue` in `GetStorageMetricsForChartBatch`) is not pinned by a test; the single-chart laziness is. Not a feature: no demand-ledger entry. |
||
|
|
1ae63559b5 |
Keep a remediation lock when a reporter gains an identity key (#2800)
Follow-up to #2783. That change made an operator's remediation lock follow a resource through manual links and `ApplyCanonicalIDSuccessions`, and named two canonical-ID changes it did not cover. Both had been read in code but not reproduced, so each was reproduced (or not) against `main` before anything changed. ## What was found **A host or Docker reporter that gains an identity key: reproduced, fixed here.** A host agent or Docker host with no machine ID, DMI UUID or cluster slot is keyed by its source-specific ID and holds no identity pin (a pin needs a strong key). When a later report carries a key the resource mints a new ID, and `identityPinIndex.successionsFor` has no predecessor pin to name, so nothing re-keys the operator's rows. The lock stays on an ID the registry no longer lists, while the dispatch gates read the listed ID's row. On `main`, the new registry test fails for all 12 variants (host agent, Docker host and Swarm join; memory and SQLite stores; never-auto-remediate and retired). Host agents advertise action capabilities, so this one is a real way past the lock. **A physical disk re-keyed in place: reproduced as an orphaned row, but not a way past the lock.** `rekeyPhysicalDiskLocked` moves a disk from the unscoped serial ID to a machine-scoped one when a second machine reports the serial, and nothing moves its operator-state row. A probe confirmed it (the disk's ID changes, the row stays behind, and the stale row reappears when the other machine's copy goes away). But a dispatch needs a capability the resource advertises: the planner plans only `resource.Capabilities`, the Assistant's control candidates exclude disks, and no producer gives a physical disk a capability (a registry-built disk had 0 in the probe). So the lock guards nothing on a disk today, and a carry for it would be dead weight with a half-covered return path. What is lost on a disk is its other operator settings (retirement, maintenance, notes), which is a different, smaller question and is left out of this change. ## What changed - `ingestRecord` notes the source-specific ID of each host agent and Docker host reporter that lands on another ID with a key (`noteReporterEraLocked`); the snapshot and record ingests hand the batch to the existing `applyRecordSuccessions` (which skips an ID still observed), exactly as Proxmox guests' retired IDs already go. The stores then carry the row, or the lock onto an existing row, once per predecessor. - Left out on purpose: Proxmox nodes (they borrow their linked agent's key, which a link or split can move either way), saved-host continuity, a source-specific ID that holds a pin (pin succession's decision), and one an operator split names (a succession would rewrite the pair into an exclusion of the resource from itself the generation the other side is absent). - `SQLiteResourceStore.ApplyCanonicalIDSuccessions` now skips a predecessor that is already recorded to any successor, as `MemoryStore` does. The table is keyed by the predecessor, so a different successor could only be refused by the insert, after a write transaction and a pin-cache invalidation on every rebuild. - The physical-disk premise is pinned: `TestPhysicalDiskResourcesAdvertiseNoActionCapability` (Proxmox, agent SMART, Unraid and record-fed disks) and `TestProviderDiskRecordsAdvertiseNoActionCapability` (the TrueNAS provider's own records). A producer that gives a disk a capability fails them with a pointer to the contract. - Contract: the two "not covered here" items in the remediation-lock paragraph are replaced by what is carried, what is left out and why. ## Limits stated in the contract - The first rebuild after an upgrade declares every keyed reporter's era and also restores a lock or row an earlier keyless-to-keyed change left behind, onto the keyed ID's existing row too, even where the operator has edited it since. A lock the operator wants gone can be cleared on the keyed ID afterwards and stays cleared. - The row moves while the previous generation is still being served (as for every succession), so an action planned against the old ID in that window finds no row there. A link the succession rewrites takes effect in the next generation. - An ID a reporter held under a report ID it no longer sends is not recomputable. A change from one machine key to another is a different machine, not an era. - An operator-split reporter's rows are left alone by this declaration. Separately, on `main`, a pin-driven succession can already erase such a split (the split Docker source's own pin holds the shared machine key and `successionsFor` names it); that is not touched here. ## Verification Run on pulse-dev: - `TestReporterGainingAStrongKeyKeepsItsRemediationLock` (host agent and Docker host gaining a machine ID, a Docker host joining a Swarm) fails on `main` and passes with the change; `TestReporterEraIsDeclaredOnlyForAKeyTheReporterGains` (keyless stays put, continuity moves nothing, record ingest, node borrow, linked keyless agent, split exclusion, pinned ID) and `TestCanonicalIDSuccessionRekeysAPredecessorOnce` pass. - Whole `internal/unifiedresources` and `internal/truenas` packages pass on the merged tree. - Mutation controls, one break per rule against the new tests, all caught: host agent not declared, Docker host not declared, continuity declares too, Proxmox node declares too, keyless reporter not filtered, pin guard dropped, exclusion guard dropped, record epilogue not drained, snapshot epilogue not drained. - `internal/monitoring` passes on an earlier revision of this change (the later revisions only add the guards above); `internal/api` did not finish inside 40 minutes at load ~80 on pulse-dev, so CI is its authority. - Two independent review passes before pushing found: the split-erasure case (now guarded and tested), a contradictory-pin case (guarded and tested), a TrueNAS tripwire that did not exercise the provider (now does), a steady-state SQLite cost (fixed), and several contract sentences that overclaimed (reworded). No demand-ledger entry: this is a defect, and neither `FEATURE_REQUESTS.md` nor GitHub issues carry a report of it. |
||
|
|
b619b5e2e4 |
Hide section tabs and lift table preview caps in kiosk (#2797)
Follow-up to #2779. Kiosk (`?kiosk=1`) is the read-only wall display from #917, and with the filter bars gone two pieces of chrome still assumed someone at the keyboard. ## What changes - `PlatformSectionTabs` stays unmounted in kiosk, on every platform page and on the Proxmox Backups `By date` / `Coverage` rail. The maintainer's replies on #917 and #1102 say kiosk hides section navigation, and the v5 Proxmox overview did too. The rail mounts fresh when kiosk ends, so its phone scroll controls bind to the new element. - `createPlatformTablePreview` shows every row in kiosk and renders no Show all button. The expansion state is kept, so leaving kiosk restores the preview as it was and an open node drawer is not stranded. - The contract records both rules next to the FilterBar kiosk paragraph, why the drawer search fields and the `Guests on <node>` reset stay, and corrects the stale preview row counts (twelve and six, not eight and four). ## Why Measured on the 50-node mock estate with only #2779 applied (headless Chromium at 1280x400, the rack LCD from #917, plus 1440x900 and a phone): - the section rail still cost the 400px display 53px of height (106px on Backups, which carries two rails); - the 12-row cap kept 38 nodes off the display, including the offline node ranked 23rd. The primary navigation and its Alerts count are hidden in kiosk, so the table rows are where a node going down shows. ## Decisions on the other kiosk gaps - Drawer search fields: kept. They exist only inside a drawer someone opened with a click, and type-to-search registers only while it is open. A stray key on an overview reaches no field (checked live), so `useTypeToSearch` is unchanged. - The `Guests on <node>` heading with its Show all nodes reset, and the Backups and Alert History chart selections: kept. A click, or a URL someone chose, can narrow the page, and these are the way back from it. - vSphere VM table title: deferred, see below. ## Not in this PR The vSphere overview's VM table has no title (Docker and Proxmox title theirs), so in kiosk it follows the Hosts table with no heading. `VmwarePageSurface.tsx` is held by an in-flight WorkloadsSurface refactor, so it is left alone here. ## Verification - Browser: 16 route and mode pairs at 1440x900, 1280x400 and 390x844, live kiosk toggling by button and Escape, an expansion with a node drawer opened past the cap surviving a kiosk round trip, the Kubernetes drawer search fields, and the node-scoped heading reset. The receipt is in the diff. - Tests: new `sharedPlatformPage.kiosk.test.tsx` (all four original cases fail on main, and two mutations of the helper fail only the intended tests), guardrail pins in `SharedPrimitives.guardrails.test.ts`, and the platform feature suites. |
||
|
|
2ac9142d85 | Merge remote-tracking branch 'origin/main' into refactor/workloads-surface-state | ||
|
|
89173fca44 | Merge remote-tracking branch 'origin/main' into refactor/postures-meta-drop | ||
|
|
7148c4869c |
Drop the unread postures response meta from the frontend
The Proxmox Backups tab reads the rows and the policy of GET /api/recovery/postures through useProtectionPostures and nothing else. The response type still declared the endpoint's pagination meta, and the hook wrote it twice: an empty one in the initial value, and one it invented after joining its 200-ID batches (page 1, the batch size as limit, the row count as total, the request count as totalPages). Nothing in src or its tests reads any of it, so those fields described a pagination the client does not have. Remove the meta from ProtectionPosturesResponse, both writers, the test fixtures that sent it, and RecoveryResponseMeta, whose last user this was once the points response dropped its own. The backend and the wire shape are unchanged. A type-level pin keeps the response to data and policy, and a new hook test joins a 201-ID fleet into two batches and checks the rows, their order, the policy and the response keys at runtime: nothing covered the join or the policy flow before. The storage-recovery contract records the rule next to the points decode. |
||
|
|
f74fa7b2fb |
Carry the PBS-reported node name into the connections ledger (#2794)
A PBS machine that was configured by IP or DNS alias still showed in
connected systems as an API row plus a separate host-agent row, even
though the PBS node and the agent report the same hostname.
## Why
The PBS poll records the hostname the node reports about itself
(`PBSInstance.NodeName`) so `/api/connections` can alias the PBS
connection by it and group the agent running on that machine into the
PBS row. The name reaches the unified PBS resource (`PBSData.NodeName`),
but `Monitor.PBSInstancesSnapshot()` rebuilds each instance from the
read-state view, and the view had no accessor for it. The snapshot
therefore always carried an empty `NodeName`, the ledger's reported-name
map was always empty, and the alias never reached `HostAliases`.
The node name was added on 2026-08-01 (
|
||
|
|
0cebf6f51f |
Require page-owned state on WorkloadsSurface
WorkloadsSurface built its own workloads state when no `state` prop arrived, and its props type extended every useWorkloadsState option. Both production mounts, the Proxmox and vSphere overviews, already build that state themselves (it also drives their one WorkloadsFilter) and pass it in, so the fallback only ran in tests. The mounts still passed vms, containers, nodes and useWorkloads, and Proxmox passed its app-container exclusion, all of which the surface ignored once it had a state. The surface now requires `state` and accepts only presentation props: forcedPlatform, forcedViewMode, the empty-state copy and tableTitle. The hook's options type is renamed WorkloadsStateOptions so it no longer shares a name with the surface's props. The pages drop the ignored props. Tests that leaned on the fallback build state through a page-owned harness, as the pages do. The Proxmox page contract test pins the exact props its mount passes. The overview guardrails check that each surface repeats the platform and view scope its page gave the state, and that the surface imports only the state's type, so it cannot call the hook again. Because the surface no longer imports the hook at runtime, Rollup now names the chunk the Proxmox and vSphere pages share after its last module, useWorkloadsState, and the bundle check reported the old WorkloadsSurface key as missing. The chunk is the same code at the same size, so the budget key follows it with its baseline and limit unchanged. |
||
|
|
ff11b6fd99 |
Keep a remediation lock when a reporter gains an identity key
A host agent or Docker host that reported no machine ID, DMI UUID or cluster slot is keyed by its source-specific ID and holds no identity pin, because a pin needs a strong key. When a later report carried one the resource minted a new ID, no pin named the old one, and pin-driven succession declared nothing. The operator's rows, a remediation lock included, stayed on an ID the registry no longer lists, while the dispatch gates read the row of the listed ID. Pulse could then act on a host the operator had told it never to touch. The retired ID is recomputable from the report itself, as a Proxmox guest's retired IDs are. ingestRecord now notes the source-specific ID of each host agent and Docker host reporter that lands on another ID with a key, and the snapshot and record ingests hand the batch to applyRecordSuccessions, which skips an ID still observed. The stores already carry the row, or its lock onto an existing row, once per predecessor. Proxmox nodes are left out on purpose: they borrow their linked agent's key, and a link or split can move that borrow either way. The other uncovered change named in the contract, a physical disk re-keyed in place to a machine-scoped ID, is not a way around the lock today: the planner plans only capabilities a resource advertises and no producer gives a physical disk one. TestPhysicalDiskResourcesAdvertiseNoActionCapability pins that for the Proxmox, agent, Unraid and TrueNAS producers, and the contract now says so, including that the disk's other operator settings are not carried. |
||
|
|
8823440f33 |
Stop mock-mode callers that read identity rebuilding the estate view
In mock mode the unified view is keyed on the fixture data version, which advances on every metric tick, and building it for the default 1,776-resource estate costs about half a second of CPU (many seconds on a starved process). #2778 moved the metrics-target lookup onto the fixture structure revision; the other callers that read only what the estate lists and how it connects still rebuilt the whole view after each tick. At startup they made 42 view requests, all misses, and with no client connected the monitor tick alone rebuilt the estate about every nine seconds. Callers that read identity and topology now share the view built for the current structure revision and operator links (currentStructureUnifiedStateView, GetUnifiedStructureReadState): the Docker-alert prune, the workload chart prewarm, the demo Patrol resolver, and the host-agent evaluation, which also lists nodes only when an offline host names a linked node. The connection-degraded alert feed keeps only platform rows, so it no longer reads the hosts, their desired-config fingerprints or the PBS instances that only feed agent rows and a host alias its snapshot drops. The mock storage chart is not one of them: a linked pool takes its capacity from whichever source was seen last, which a tick can move without a structure change. It keeps the data-version view, read once per request instead of once per uncached pool. The per-node polling lookups of a monitor that keeps real polling in mock mode (linkedHostForNode, getHostAgentTemperatureForNode, carriedTemperatureOutlivesAgentLease) read a host's status, disks and sensors, which a tick moves, so they stay on the data-version view. On pulse-dev (one pinned core, host load 30 to 110, probe builds): a start built the estate view twice before the listener answered with the wiring change alone and once with this; with no client connected the process built the estate 33 times in 299 s and used 12.3 s of CPU over a 180 s window, against twice and 3.3 s with this change. |
||
|
|
55443c7095 |
Build the mock linked view at the rebuild's link boundary
The monitor's rebuild holds the operator-link pass back until the snapshot and every record source except availability are ingested, then judges each chain once, so a fold cannot be locked in before a later source brings a member that outranks it. The mock-mode fixture build still ran a pass per ingest call, so a chain whose answer depends on the order its members' sources arrive in (a Proxmox storage, a TrueNAS VM and a vSphere VM linked in a cycle) could settle on a different row than the live rebuild: mock mode kept the vSphere VM where the rebuild keeps the TrueNAS VM. Export the registry's deferral as DeferManualLinks and ApplyDeferredManualLinks (the latter takes the stale thresholds the ingests were given, as the Ingest* calls do) and have the fixture build use them: defer, ingest the snapshot and the TrueNAS and vSphere records, run the held-back pass once, then ingest availability so a check linked to a folded resource still projects onto its primary. A registry with no links defers nothing, so the shared memoized snapshot is built exactly as before; a linked build pays one extra refresh round (about 3 ms of 52 ms on the default estate), the same round the rebuild runs. TestFixtureGraphAppliesManualLinksAtTheRebuildsBoundary replaces the per-ingest-stage test: it keeps the availability projection and pins the cycle against a real MonitorAdapter rebuild, with a per-call control so a fixture change cannot silently stop separating the two. The unifiedresources tests pin the exported pair, including that the deferred pass judges freshness by the thresholds it is given. The contract also drops a stale line: a check that names a folded resource by canonical ID follows the fold to the primary, as TestAvailabilityLinkToALinkFoldedResourceFollowsItsPrimary pins. |
||
|
|
4471dcaaba |
Fold chained manual links the same way in any link order (#2793)
Operator links that share a member form a chain, and `applyManualLinks` used to apply them one at a time in store order, looking each member up only among listed rows. Once an earlier link folded a member into another row, a later link naming that member skipped. So a chain depended on the order its links were stored in: - Agent A linked into VM V, and V linked into storage W. A-V first folds A into V and V into W. V-W first folds V into W, and A-V then finds V gone, so A stays its own row. - A saved host (continuity-only row) whose partner the published registry had already folded into a third row answered for itself instead of being held under that row. Standing the row that holds a folded member in for it (an earlier attempt) fixed neither: that row then outranked the link's own primary, and the agent-into-guest direction was lost once the guest was folded. ## What changes Links now apply as chains (`internal/unifiedresources/manual_link_chains.go`). - A link member resolves to its own row or the row a link folded it into, and is judged by its own recorded shape (agent, hypervisor-managed guest), not the shape of the row holding it. Shapes ride on the fold records. - Every connected set of observed members folds into one root. Candidates are the members no link folds (every member on a direction cycle; the chain's guests when every candidate is an agent). Candidate rows compete by their own identity: guest, then non-agent over agent, then the earliest-created link, then canonical ID. A single link and an agreeing chain keep their last primary, as before. - The spanning tree follows the fold records rows already carry before any other link, so a refresh that recreates folded members folds them back along their records. A row folding into another orients its records away from an anchor (the surviving row's ID when its records name it, else the linking member), so the surviving row is never recorded as folded and every folded ID keeps resolving. - Every link of the chain is recorded on the root (cycle-closing links included, with each side's own sources), so report-merge can cut a reported member from every path. - Saved members never join a chain; they are held under the row their partner survives in, found after the folds, by the same precedence. - A joined Proxmox node+agent row records the fold of the node's own ID before links resolve, so a second link naming it joins the chain in either store order. A link a newer operator split separates is dropped before the chain forms (both behaviours were already in the loop and are kept). - The monitor's rebuild holds the link pass back until the snapshot and every record source except availability are ingested, then folds each chain once over the assembled estate. Availability still comes last. Live supplemental refresh and the read-state overlay still run a pass per call. ## Evidence New proofs rebuild each estate in every link order and compare the monitor, its read-state overlay and a registry seeded from that read state, then refresh each record source. Negative controls on pulse-dev (each reverted): - On main, 10 of 12 chain cases, the refresh case, the joined-node case, the changed-link seed cases, the surviving-row hold case, the rebuild case and the report-merge cycle case all fail; the other two chain cases guard behaviour that already worked. - A plain breadth-first tree, a path-only rehang, ranking a pre-fold row for holds, no shapes on the joined-pair fold, recording the joined-pair fold during link discovery, forcing the rebuild back to per-source passes, dropping cycle-link records, and not carrying own sources each fail the specific test written for them. - Full `internal/unifiedresources` passes on the final tree (326s under load); `internal/monitoring`, `internal/api/resourceapi`, `internal/api` link/continuity/report tests, `internal/mock`, `internal/vmware` and a full build passed on the tree before the last rebase. Two single failures under machine load (`TestRetentionLoop_RunsInitialPrune`, `TestJSONRPCSnapshotContinuesAfterTelemetryTimeout`) passed 6/6 and 8/8 in isolation, with main's link files as well. ## Not in this change - The mock-mode fixture build (`internal/mock`) and the resources API's snapshot-seeded fallback still run a pass per source, so a chain whose answer depends on the order its members' sources arrive in can settle differently there than in the monitor's rebuild. The contract says so; a follow-up aligns the mock build. - Production reaches the resources API's snapshot fallback only when the monitor has published no unified listing. |
||
|
|
199e13e278 |
Keep an operator-split host pair as two rows in the browser (#2792)
The browser re-joined same-hostname agent and platform rows from the realtime payload without knowing the operator's merge exclusions, so a node and agent that "Split merged resource" (report-merge) or unlink had split on the server still rendered as one row. Every realtime path (the websocket snapshot, its deltas and `/api/state`) already carries the server's presentation coalesce, which honours the exclusions and provider scope, so the browser now merges each row only with the previous row of its ID. This replaces the "browser's realtime coalesce" limit sentence that #2741 added to the unified-resources contract. Showing the split exposed three ways the surviving row kept the departed source's data, which the Machines page reads as provider ownership (it answered "No Pulse Agent machines"): - A row whose own `sources` list lost a source is rebuilt from the incoming row, keeping the REST-only scopes, source status and readiness of the sources that remain. A previous row claims every source either of its two lists names, so a half-updated row is rebuilt as well. - Source status entries follow the sources the merged row lists. - The resources hook's full merge handed the websocket store's rows to the merge, so cache rows shared a nested `sources` array with rows the store reconciles in place and flipped to the split value while keeping their joined-era enrichment. It now clones the store's rows on that path. Verification: - Private stack (the server from #2741, a fake Proxmox API, a test agent): the split is delivered live with no reload and the node stays on the Proxmox page while the agent appears on Machines with a clean drawer, at 390 and 1280 wide. On main's frontend the agent was drawn as a second Proxmox node and Machines showed "No Pulse Agent machines". Before the second and third fixes the agent was missing from Machines in 2 to 4 of 4 phone-width runs. The browser receipt states which revision each pass ran on. - New and changed tests pin a split pair through full snapshots and deltas in the adapter, the websocket store and the resources hook, and each rule above has a test that fails without it. - Reviewed by Sol over seven rounds. The last rounds' remaining findings concern which REST-only scope and readiness data survive a rebuild when the realtime payload carries no provenance; the contract states those as known limits (the next REST refresh restores the data), alongside two others: a hook's own store is still created from its cache's rows, and the fast merge path is content-equivalent to the full path only for a display row whose own source lists agree. Open PR #2699 edits the same adapter and already conflicts with main. |
||
|
|
884694a046 |
Carry the PBS-reported node name into the connections ledger
The PBS poll records the hostname the node reports about itself on PBSInstance.NodeName so /api/connections can alias the PBS connection by it and group the host agent running on that machine into the PBS row. The name reaches the unified PBS resource (PBSData.NodeName), but Monitor.PBSInstancesSnapshot() rebuilds each instance from the read-state view, and the view had no accessor for it, so the snapshot always carried an empty NodeName. The ledger's reported-name map was therefore always empty and a PBS machine configured by IP or DNS alias still showed as an API row plus a separate agent row. The unit tests missed it because they hand the aggregator its inputs instead of going through the monitor. Add PBSInstanceView.NodeName() and copy it in pbsInstanceFromReadStateView. The new ledger test drives a real monitor through the input builder, the connection aliases and the system grouping: with the conversion line removed it fails on the missing alias, with it the agent attaches to the PBS system. Only direct root@pam sessions can read the node name (API-token connections get none), so this restores grouping for password-auth PBS connections; token-auth ones still match only on what they were configured with. |
||
|
|
c65e412cc1 | Merge remote-tracking branch 'origin/main' into fix/browser-coalesce-honours-split | ||
|
|
33184012ce |
Keep an operator-split host pair as two rows in the browser
The browser re-joined same-hostname agent and platform rows from the realtime payload without knowing the operator's merge exclusions, so a node and agent that report-merge or unlink had split on the server still rendered as one row. Every realtime path (the websocket snapshot, its deltas and /api/state) already carries the server's presentation coalesce, which honours the exclusions and provider scope, so the browser now merges each row only with the previous row of its ID. Showing the split exposed three ways the surviving row kept the departed source's data, which the Machines page reads as provider ownership and answers with "No Pulse Agent machines": - A row whose own sources list lost a source is rebuilt from the incoming row, keeping the REST-only scopes, source status and readiness of the sources that remain. A previous row claims every source either of its two lists names, so a half-updated row is rebuilt too. - Source status entries follow the sources the merged row lists. - The resources hook's full merge handed the websocket store's rows to the merge, so cache rows shared a nested sources array with rows the store reconciles in place and flipped to the split value while keeping their joined-era enrichment. It clones the store's rows on that path. On a private stack (server with the split override, a fake Proxmox API and a test agent) the agent stayed on the Machines page in every final transition at 390 and 1280 wide; before the last fixes it went missing in up to half of them. The browser's realtime coalesce limit in the unified-resources contract is replaced by the new rule and its known limits. |
||
|
|
95ff7f2804 |
Fold chained manual links the same way in any link order
applyManualLinks applied operator links one at a time in stored order and looked each member up only among listed rows. Once an earlier link folded a member away, a later link naming it skipped, so a chain depended on store order: an agent linked into a VM that a second link folds into storage joined the chain in one order and stayed its own row in the other. A saved host whose partner the published registry had already folded answered for itself instead of being held under that row. Links now apply as chains. A link member resolves to its own row or the row a link folded it into, judged by its own recorded shape (agent, hypervisor-managed guest), so the agent-into-guest direction survives a folded guest. Every connected set of observed members folds into one root: members no link folds are the candidates (all members on a direction cycle, the chain's guests when every candidate is an agent), ranked by the row each would leave: guest, then non-agent, then earliest-created link, then canonical ID. A single link and an agreeing chain keep their last primary. The spanning tree follows the fold records the rows already carry before any other link, so a refresh that recreates folded members folds them back along their records and keeps every folded ID resolving; every link of the chain is recorded, so a report-merge can undo a cycle. Saved members never join a chain and are held under the row their partner survives in, by the same precedence. The monitor's rebuild now holds the link pass back until the snapshot and every record source except availability are in, so a chain is judged over the whole estate rather than folded as its members' sources arrive. The proofs rebuild each chain in every link order and compare the monitor, its read-state overlay and a registry seeded from that read state, then refresh each record source. The mock-mode fixture build and the resources API's snapshot-seeded fallback still run a pass per source, which the contract states. |
||
|
|
e63e50fe13 |
Carry operator decisions across a rebuild that cannot read them
A registry whose store read for manual links or exclusions failed loaded none of them, and the monitor adapter published that registry anyway. For that generation a pair the operator had split listed as one machine again, manual links unfolded, and the node/agent split decider answered "undecided", so monitoring relinked a split pair until the next rebuild that could read the store. A rebuild now takes the links and exclusions of the generation it replaces when either read failed, and so does the host-continuity overlay. The resources still come from the new snapshot: stale resource data is worse than stale decisions. The next readable rebuild loads the store's current decisions, so a split recorded or lifted during the outage still lands. The resources API rebuilds its own registry from the store and is not covered here. |
||
|
|
ba91fc6699 |
Keep guest disk totals, email settings and recovery guidance trustworthy (#2790)
## What changed and why - Count a guest volume once at its highest accepted usage, independent of alias mount order, while retaining each mount's readings. Reject case-variant filesystem identity fields that could overwrite a real mount/type and hide it from an apparently complete inventory. Extend both collectors' assertions through serialized totals and mount rows. - Show only email settings the server persists. Remove unsupported Reply-to and retry controls, explain server-managed retries, and preserve supported SMTP/TLS/rate settings and editing while email is off. - Explain destructive agent removal, separate action-runner revocation, retained recovery material and partial failures. Add safe containment guidance after an accidental public upload: issuer revocation and attachment removal are separate, deletion cannot guarantee erasure, and monitoring coverage must be preserved. - Discover closed pull-request CI in requested, waiting and pending states as well as queued/running work, retaining exact-PR, fresh-closure, branch-reuse and refusal checks. These are repairs to existing source behavior and guidance omissions, not new features or claims of installed recovery. ## Included reviewed source Complete unpublished range: `e4da1b037000e4eb4b6ea7089704993c74708db8..dd46bf916f46c63accdee3aa6b26d68fdb2edf50`. Candidates: - `6075dec7f00cfcbbbb59eee23f01f42a90b97bf8`, composition `3747c98e718afe90e8ff6186e90b88fee9dfc885`: alias usage. - `cce0cfd5a72b628e3b9219bd26993aaaf42b3f66`: serialized collector assertions. - `e7d8092c633a5ed3379ef52d0194b3706d20219e`: canonical identity fields. - `2526d7dff21f79525e955e019d470c7dbd4f251f`: persisted email settings. - `e5f27d25f61aa81c84d0409790851c0fa0e132c9`, `da269aeaf272a048cdbb66afddb0172ac3f54467`: removal guidance and recipe inventory. - `6409a38480992a6dacb09df8f4d40515887998e1`: disclosure containment guidance. - `50b5345be8f5e9a583d939784ce64118099d3c7c`: closed-PR held-run cleanup. All previously reviewed commit identities and ancestry are retained. Final additive merge `dd46bf916f46c63accdee3aa6b26d68fdb2edf50` incorporates current published upstream `e4da1b037000e4eb4b6ea7089704993c74708db8`, including its scheduler-cadence freshness repair; no accepted commit was rewritten. Later, separately unqualified notification-draft work is not included. ## Validation and remaining limits - Final-source provenance, whitespace, per-commit canonical-history checks and range browser-evidence guard pass. The existing SMTP browser receipt covers the final four changed runtime files, its original parent, desktop/phone layouts and supported save/reload behavior; it was not rebound or repeated. - Final lightweight fixture checks pass: closed-PR cleanup controls, twelve removal/containment tests, public documentation (243 Markdown files) and shipped mirrors (62 documents). - Retained candidate evidence includes normal/race canonical-metadata decoder checks (94 identities/six roots), 24 parent failures with compatibility controls, connected alias/serialized-row collector, served-state, History and breach/zero-recovery checks, focused SMTP/browser proof, and 52 held-run cleanup controls with ten parent failures. These remain candidate evidence, not a new full-source pass. - The final committed-source frontend command requested offline install, types, lint, full tests, build and bundle budgets. VM admission returned `proof-capacity-low`, client exit 2, before execution: no executed stage or passing receipt. It was not retried through another executor. - Earlier failed SMTP full-suite/Docker-test evidence and incomplete wider/build/removal-proof observations remain adverse or unknown. An older passing frontend run on differing SMTP source does not clear them. Exact-head hosted frontend, backend, documentation and governance checks must resolve containing-source acceptance before landing; no check is waived by this proposal. No native notification delivery, guest thaw, ordinary installed polling/recovery, credential revocation, live CI cancellation, deployment or release readiness is established here. [How Pulse handles triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md) |
||
|
|
4faba67f66 |
Keep a split PVE disk off the agent's SMART row (#2789)
## What
A merged physical disk that the operator splits ("Split merged
resource", `POST /api/resources/{id}/report-merge`) now stays split for
the PVE disk poller too. #2748 made the registry honour the exclusions
and listed the poller as the part of the split it did not reach:
`mergeHostAgentSMARTIntoDisks` paired the linked agent's SMART row with
the Proxmox disk by WWN, serial or path before the registry saw either,
and read no exclusions. The split Proxmox row kept the agent's
attributes, I/O, health, any serial, WWN, type or temperature it filled,
and `AgentSMARTReported`, which hands the disk's temperature alert to
the agent's `CheckHost`.
## How
- `mergeHostAgentSMARTIntoDisks` asks the resource store
(`MonitorAdapter.ProxmoxDiskAgentSMARTSplit`, new
`internal/unifiedresources/physical_disk_agent_split.go`) once per
matched row, on the full poll and on the skipped polls between. The
answer is `physicalDiskSplitLocked`'s, applied to the agent's disk as
the registry generation holds it and to the Proxmox observation under
its own ID, so the exclusions that keep the registry's rows apart keep
the poller's pair apart, in the same forms:
- an exclusion naming only the agent's candidate splits nothing;
- a replacement disk in the slot inherits no split recorded against the
disk it replaced;
- a manual link that leaves the generation holding both observations as
one disk is the operator's later decision, so the poller pairs them
again.
- A split disk takes nothing from the row: no attributes, I/O, health,
identity, type or temperature, and no `AgentSMARTReported`. It also
takes no agent-supplied node-sensor temperature: the node poll takes the
node's SMART list from the linked agent while the agent is reporting and
its report carries a usable temperature, so `mergeNVMeTempsIntoDisks`
would otherwise hand the agent's row of the other disk to the split one
and the PVE check would alert beside the agent's own alert. An
SSH-supplied reading stays, and the PVE disk check judges it.
- The record is marked `AgentSMARTSplit` (internal poll evidence, never
serialized), so the poll's evidence retention restores nothing into it.
- The Proxmox-query-failure fallback (`physicalDisksFromHostAgentSMART`)
gives a split row no stand-in.
## Limits (named in the unified-resources contract)
- The split applies on the first full disk poll that collects the node
from Proxmox and matches the agent's row; a record already merged keeps
the agent's data until then (five minutes by default).
- A split disk retains no last-known serial or ZFS pool through a poll
that cannot collect them: nothing records which retained value Proxmox
collected and which the agent filled.
- While the Proxmox query fails, a split disk's Proxmox record is not
listed (its stand-in would be the agent's disk again).
- The query-failure fallback judges its stand-in row by the row's own
identity, so it does not apply a split that names identity only the
Proxmox disk carries (an agent disk in standby).
- A split recorded under one key of a controller member (the stand-in's
target-qualified key against a Proxmox poll's device-only key) names an
observation the other key's rows do not carry, as for the registry.
## Verification
- `TestOperatorSplitKeepsAgentSMARTOffTheProxmoxDisk` drives the poller
through four report-merge shapes (both candidates, the Proxmox candidate
alone, the agent's candidate alone, an unrelated exclusion) for three
disks (SAS paired by path, NVMe paired by serial, NVMe whose node
reading is SSH-supplied), on full and skipped polls, with an open PVE
temperature alert that the pairing closes and the split does not bring
back, the health alert the agent's failure raised, and a failing Proxmox
disk query.
- Unit tests pin the merge (including Proxmox's own health and wearout,
fields the agent must not copy, and the own-ID rule for controller
members), the fallback, the node-sensor drop and the source labels it
relies on. `TestOperatorSplitSeparatesDisksJoinedInsideALinkedHost` also
asks the decider at each step (relink, standby, replacement, a
generation that has not ingested the agent's disk).
- Negative controls, one mutation per rule (18), each turned a named
test red; the full `internal/models`, `internal/unifiedresources` and
`internal/monitoring` suites ran on pulse-dev against the merged tree.
`TestGuestWindowsMeminfoPollingContract` failed once in a loaded full
run and also fails on pristine `main` under the same load (it passes
alone on this branch), so it is not part of this change.
|
||
|
|
6ac7c42d2e | Merge remote-tracking branch 'origin/main' into fix/kiosk-section-chrome | ||
|
|
dd46bf916f |
Incorporate current polling freshness repair without rewriting reviewed work
Preserve the complete accepted local maintenance range and add the current published scheduler-cadence repair. Final containing validation and exact-head protected checks remain required. Change-source: pulse-maintainer |
||
|
|
ff56e7583d |
Incorporate batch-start published upstream
Preserve locally reviewed commits and import the upstream frontier observed for this bounded coordination turn. The unqualified alert-draft candidate remains outside canonical source. Change-source: pulse-maintainer |
||
|
|
ec50758974 |
Keep a split PVE disk off the agent's SMART row
Splitting a merged physical disk (the drawer's "Split merged resource",
POST /api/resources/{id}/report-merge) recorded its exclusions and the
registry kept two rows, but the PVE disk poller had already paired the
linked agent's SMART row with the Proxmox disk by WWN, serial or path
before the registry saw either, and read no exclusions. The split Proxmox
row still carried the agent's attributes, I/O, health, any serial, WWN,
type or temperature it filled, and AgentSMARTReported, which hands the
disk's temperature alert to the agent's CheckHost. #2748 listed this as
the part of the split it did not reach.
mergeHostAgentSMARTIntoDisks now asks the resource store
(MonitorAdapter.ProxmoxDiskAgentSMARTSplit) once per matched row. The
answer is physicalDiskSplitLocked's, applied to the agent's disk as the
registry generation holds it and to the Proxmox observation under its own
ID, so the exclusions that keep the registry's rows apart keep the
poller's pair apart: an exclusion naming only the agent's candidate
splits nothing, a replacement in the slot inherits no split recorded
against the disk it replaced, and a manual link that leaves the
generation holding both observations as one disk pairs them again.
A split disk takes nothing from the row. That includes the node-sensor
temperature the agent supplied: the node poll takes its SMART list from a
reporting agent whose report carries a usable reading, so that reading is
the agent's row of the other disk; an SSH-supplied reading stays and the
PVE disk check judges it. The record is marked AgentSMARTSplit, so the
poll's evidence retention restores nothing into it, and the
Proxmox-query-failure fallback gives it no stand-in row.
The split applies on the first full disk poll that collects the node from
Proxmox and matches the agent's row; the unified-resources contract names
that lag and the other limits.
|
||
|
|
dc93d0adea |
Judge adaptive-polling freshness by the scheduler's cadence
When adaptive polling is enabled (ADAPTIVE_POLLING_ENABLED or system.json; off by default, no UI), the scheduler picks PVE, PBS and PMG intervals itself and never reads the per-platform ones. A healthy instance's staleness score is near zero after every success, so its cadence stretches toward AdaptivePollingMaxInterval (5 minutes by default). Resource freshness was still derived from the per-platform intervals, with floors of 60s for Proxmox and 120s for PBS and PMG, so healthy rows read stale for much of every cycle. A live stack with a fake PMG polled 3.5 to 4.3 minutes apart and the row sat degraded with telemetry_stale from two minutes after each poll until the next one. The PVE node offline grace and the node temperature carry window derive from the same cadence and lapsed between polls too. Monitor.resourcePollIntervals now judges all three sources by the scheduler's own maximum interval, which bounds every interval it selects. ResourceStaleThresholdsForConfig reads the same normalized bounds through a normalizedSchedulerConfig shared with NewAdaptiveScheduler, so callers without a monitor agree with the one that has it. The same live stack stayed online for 14 minutes. Fixed-cadence behaviour is unchanged. Per-instance planned intervals were not used: they are jittered and shrink as a poll runs late, so a per-source threshold built on them would flap. The contract note this replaces said freshness keeps deriving from the per-platform intervals; the user doc now says what adaptive polling does to freshness and offline detection. The cost is a longer node hold in adaptive mode (10 minutes with the defaults). A valid /cluster/status report that a member is offline still applies immediately. |
||
|
|
cd3904c89f |
Drop the unread points response meta from the recovery decode (#2787)
After the `useRecoveryPoints` trim in #2773 the TrueNAS Protection tab reads only the points of `GET /api/recovery/points`. The frontend decode still carried the response's pagination meta (`page`, `limit`, `total`, `totalPages`) end to end, and nothing reads it. This removes that decode. The backend response and `internal/api/recovery_handlers.go` are unchanged. ## What goes - `normalizeRecoveryMeta` and the `meta` field of `normalizeRecoveryPointsResponse` (`utils/recoveryPlatformModel.ts`) - `meta` on `RecoveryPointsResponse` and `RecoveryPointsTransportResponse` (`types/recovery.ts`) - `meta` in the hook's `initialValue` (`hooks/useRecoveryPoints.ts`) - the meta fixtures and the meta-normalization tests in `recoveryTransport.test.ts`, `recoveryPlatformModel.test.ts` and `recoveryPlatformModel.branchcov2.test.ts` `RecoveryResponseMeta` stays: `ProtectionPosturesResponse` (the `/api/recovery/postures` response) still declares it. That response has the same shape of problem (its `meta` is written by `useProtectionPostures.ts` and read nowhere); it is a different endpoint and tab, so it is left for its own change. ## Proof that nothing reads it A whole-program TypeScript checker scan over `src` and tests (2,458 files), run on the tree before the edit: - `RecoveryPointsResponse.meta`: 0 reads. Written only by the decode and the hook's `initialValue`. - `RecoveryPointsTransportResponse.meta`: 1 read, the decode itself. The other references are test fixtures. - `page`, `limit`, `total`, `totalPages` on `RecoveryResponseMeta`: read only inside `normalizeRecoveryMeta`. - No response is spread, iterated or serialized; the query layer treats the value as opaque (its one spread is the cache-entry wrapper). - Positive control: `RecoveryPointsResponse.data` is found read in the hook and in a test. Negative control: a planted file reading `.meta` eight different ways (property, destructure, element access, indexed-access type, spread, `Object.keys`, nested member, literal write) was found 8 of 8. - Object-literal keys are resolved through the contextual type; the checker returns the literal's own symbol for a key and would report a false zero otherwise. After the edit, `tsc --noEmit` over `src` (tests included) passes, so no reader can have been left behind. The scan after the edit shows the points types with `data` only and `RecoveryResponseMeta` with no readers at all. ## Pins `recoveryTransport.test.ts` (the registered proof for this path) now pins: - the keys of both points response types equal exactly `'data'` (type level; fails under `tsc --noEmit`) - a backend-shaped response that includes `meta` decodes with `toStrictEqual` to the points alone Single-flaw mutations each fail their own pin: optional `meta` re-added to the normalized type, to the transport type (tsc); a decode that copies `meta` through, and one that emits `meta: undefined` (vitest, both the transport and branchcov2 tests); `meta` back in the hook's `initialValue` (tsc, excess property). The unmutated tree passes all checks. ## Contract `storage-recovery.md`: the clause that 2773 added about the hook now also says the decode carries the points alone, that the endpoint still returns its pagination meta (backend contract unchanged), and that a surface that pages the points adds the meta back with its reader. Checked `frontend-primitives.md`, `unified-resources.md` and `api-contracts.md`: none describes the frontend decode as carrying meta; api-contracts item 34 is the backend's pagination meta and stays. ## Build, coverage and browser evidence Before/after production builds on pulse-dev (before is the #2773 head, after is the same tree plus this change): - 189 files each side: 94 byte-identical (the CSS included), 94 differ only in chunk-hash names and SRI strings, 1 differs in code. That one is the TrueNAS page chunk, 68,278 to 67,891 bytes, and after normalising hashes and short identifiers it loses only the removed statements (the meta normalizer, the decode's `meta` field, the hook's `initialValue.meta`). - `check-bundle-size.mjs` passes on both with no chunk renamed and `.bundlesize.json` untouched: TrueNAS 16.57 to 16.49 kB gzip, total JS 1372.50 to 1372.47 kB, CSS unchanged. - Coverage of the hook and the decode over the recovery suites, matched by normalised source text, loses nothing (`LOST_TOTAL 0`); 253 tests before, 252 after. - Browser pass of `/truenas/overview` and `/truenas/protection` at 1440x900 and 390x844, two runs per build. Rows, labels, status filter, search, detail panel and the open-time and poll request URLs are identical in every run; every `/api/recovery/points` response in both builds still carries `meta` (`limit`, `page`, `total`, `totalPages`). Each build had one run where a section-tab click timed out at 30 s under pulse-dev load of roughly 40 to 60, so that is the rig, not the change; the other run per build completed every step. Duplicate request counts when returning to the tab also vary within the before build (3 and 1 on desktop). Details are in the receipt under `frontend-modern/browser-verification/`. The local `tsc --noEmit` over `src` passes, as do eslint and prettier on the touched files, the canonical platform audit and the 17 targeted test files. A read-only second review of the staged change found no issues. Not in this change: `ProtectionPosturesResponse.meta` (and with it `RecoveryResponseMeta`) is written by `useProtectionPostures.ts` and read nowhere; that is the `/api/recovery/postures` hook behind the Proxmox Backups tab and is left for its own change. |
||
|
|
4239b6433d |
Integrate reviewed public disclosure containment guidance
Change-source: pulse-maintainer |
||
|
|
81013369e9 |
Stop monitoring linking a node to an agent the operator split off (#2786)
## Problem #2741 made an operator's split of a Proxmox node and its pulse-agent (report-merge / unlink in the resources API) split the registry's rows. The monitor's own link (`Node.LinkedAgentID`, `Host.LinkedNodeID`) stayed in place, so everything that reads it kept treating the two machines as one: - the physical disk poll used the agent's SMART inventory as the node's disk fallback, and applied the agent's `--disk-exclude` patterns to the node's disks - `linkedHostForNode` fed the node's disk sources from the agent - guests inherited the agent for discovery and commands - deployment reported the node as already running an agent - service discovery routed through the agent - the agent's container filesystem readings were applied to the node's containers - the node's temperatures came from the agent, through the link or the exact-hostname fallback - `sharedSystemAlertCorrelationForHost` tied the node and the agent together ## Decision An operator split should stop the monitor treating the pair as linked. Two stores were recording one fact: the resource store's exclusions and the monitor's link. The resource store's exclusion is now the one record, and the monitor follows it: - `MonitorAdapter.ProxmoxNodeAgentSplit` implements a new `models.NodeAgentSplitDecider`. It applies the registry's own rule for a node's link (`splitProxmoxNodeLink`), also under the ID the registry holds the agent under, and confirms a split it finds against the store's latest decisions, so a relink takes effect on the next poll. - `models.State` holds both sides of a split link back on every link write: the node poll (for the polled nodes and for other connections' retained nodes, before their links can steer where views fold), the reconcile after manual intent, `UpsertHost` and `LinkNodeToHostAgent`. Report ingest drops a split link before it applies it, uses the link the state kept, and clears the agent's cached container readings. Those readings are also not shown for a node that no longer names the agent. The temperature read skips a split agent in its exact-hostname fallback. - Reverse case: the agents API's node link (`/api/agents/agent/link`) is the operator's newer "same machine" decision. It now removes that pair's split from the store (`TakeExclusion`, which returns the rows it actually removed) before it records manual intent, and puts them back with `RestoreExclusion` if the intent fails to persist. A restore never replaces a decision the operator recorded in between. - A split recorded after a manual link ends that intent on the agent's next report that finds the split confirmed in the store, so host continuity stops reserving the node for that agent. An unreadable store confirms nothing and keeps the intent. - The agents API's unlink needs no store write: it clears monitoring's link, and the registry only joins a node and an agent through a link one of them declares or one inferred from another declared link. Limits (recorded in the contracts): a rebuild that cannot read the store's decisions loads none, in the registry as in monitoring, which needs a change in `registry.go` and `monitor_adapter.go` and is left for a follow-up; a carried temperature reading from an agent that was tied to its node by address stays until the ordinary carry window ends; mock mode's fixture graph keeps its own links. ## Tests - `TestMonitorStateHoldsBackAnOperatorSplitNodeAgentLink` runs #2741's six split requests over four link shapes, through a `models.State` holding the adapter's decisions. Each step runs three poll-and-rebuild cycles and checks every view (monitor listing, broadcast coalesce, resources API from listing and from snapshot), the state's links, that a report alone keeps a split pair apart, a relink, an agent-first rebuild that persists its pins, and the request again. - `TestMonitorStateHoldsBackSplitLinksItStartsWith`, `TestMonitorStateKeepsASplitWhenTheAgentStopsReportingItsMachineID` and the two `internal/models` tests (a split agent can no longer fold two connections' node views, for the polled and for the retained node). - `TestOperatorSplitStopsMonitoringTreatingNodeAndAgentAsLinked` follows report-merge through the real readers listed above, with a negative control (an unrelated exclusion keeps every one of them linked). `TestSplitAgentSensorsDoNotReachTheNodeThroughItsName`, `TestAgentContainerReadingsAreNotShownForANodeThatNoLongerNamesTheAgent` and `TestReportUsesTheLinkTheStateKeptWhenASplitLandsMidReport` cover the temperature fallback, the cached readings and a split landing mid-report. - `TestManualNodeLinkReplacesAnOperatorSplit`, `TestManualNodeLinkKeepsTheSplitWhenItsIntentCannotPersist`, `TestManualNodeLinkSurvivesAnUnreadableSplitStore` and `TestConfirmedSplitOfAManualLinkClearsTheAgentsContainerReadings` cover the agents API link before and after a split, a failing intent, an unreadable store and the intent ending. - Store tests for `TakeExclusion` / `RestoreExclusion` in SQLite and memory, including a removal that fails part way, an operator replacing the pair between listing and removal, rows stored in the reversed order, and a stale older link beside the exclusion. - Mutation controls: I disabled each of the 26 rules above in turn (state and proposal hold-back, retained-node hold-back, the `UpsertHost` checks, the report-ingest checks, ending and keeping manual intent, the lift, the restore, the store confirm, the held agent ID, the temperature filter, the cached-reading checks, and so on). Each made at least one of these tests fail. - The full `models`, `unifiedresources`, `monitoring`, `config`, `alerts`, `servicediscovery` and `resourceapi` suites pass on pulse-dev; `TestGuestMetadataWriterFailuresAreOwnedWithoutSpinRetry`, `TestDeliveryEpisodeCoalescingSurvivesRestart` and `TestTrueNASPollerUnresponsiveRPCDoesNotFreezeOtherConnections` failed once under load average ~80 in packages this change does not touch, and each passes alone. |
||
|
|
7c9daa000b |
Integrate canonical guest filesystem identity repair
Accept the bounded canonical-key admission repair with retained focused normal, race and exact-parent audit evidence. Existing completeness handling remains unchanged; connected and native recovery are not claimed. Change-source: pulse-maintainer |
||
|
|
49fd0b5d61 | Merge remote-tracking branch 'origin/main' into refactor/recovery-meta-decode | ||
|
|
fb84cdaebf |
Drop the unread points response meta from the recovery decode
After the useRecoveryPoints trim in #2773 the TrueNAS Protection tab reads only the points. The decode still carried the endpoint's pagination meta (page, limit, total, totalPages) through normalizeRecoveryMeta, the meta field on both points response types, the hook's initialValue and several test fixtures, none of which anything reads. A whole-program checker scan over src and tests (positive control: the data member, read in the hook and a test; negative control: eight planted readers all found) showed zero reads of the normalized or transport meta. The one read of the transport meta was the decode itself, and the four RecoveryResponseMeta members were read only inside normalizeRecoveryMeta. The response is never spread, serialized or iterated, and the query layer treats the value as opaque. Remove the decode, the meta fields on RecoveryPointsResponse and RecoveryPointsTransportResponse, the initialValue meta and the meta fixtures. RecoveryResponseMeta stays: the protection postures response type still declares it. recoveryTransport.test.ts pins the new shape: the response types' keys are exactly data (a type-level pin that fails under tsc), and a backend-shaped response that includes meta decodes to the points alone. The backend response and internal/api/recovery_handlers.go are unchanged, and the contract prose now says the frontend decode carries the points alone. A before/after production build pair differs in one chunk (the TrueNAS page chunk, 387 bytes smaller) and only by the removed statements; the CSS and the other 93 files are identical apart from chunk hashes. The TrueNAS Protection tab shows the same rows, filters, search results and detail panel at desktop and phone width, and the backend still sends meta in every response. Coverage of the hook and the decode loses nothing but the removed meta statements. |
||
|
|
0ddeef2785 |
Compose published remediation lock repair with reviewed monitoring maintenance
Preserve the exact reviewed local tip and current upstream ancestry. Incorporate the independently published link and succession lock carry repair additively; final-source proof remains required before publication. Change-source: pulse-maintainer |
||
|
|
1343f23aa7 |
Incorporate current upstream without rewriting reviewed maintenance
Preserve reviewed SMTP, removal guidance and filesystem integrations while composing current Patrol, policy coordination and Storage repairs for publication. Change-source: pulse-maintainer |
||
|
|
809f0434b8 | Merge remote-tracking branch 'origin/main' into fix/kiosk-section-chrome | ||
|
|
61ffbdc29a |
Keep a remediation lock across links and ID successions (#2783)
## What was wrong An operator's remediation lock (`NeverAutoRemediate`, or `LifecycleState=retired`; `BlocksRemediation()`) is a row in `resource_operator_state` keyed by one canonical ID. The planner, the Patrol action broker and the dispatch gate (`actionlifecycle.validateExecutionPolicy` → `ErrResourceRemediationLocked`) read the row of the ID a resource currently has. Two identity changes left the locked row behind: 1. **Manual link fold.** `applyManualLinks` merges the linked resource into the primary and deletes it from the registry. Only the surviving ID is ever acted on, and nothing consulted the folded ID's row. The registry may also keep either side (a guest outlives an agent running inside it whichever way the link points). 2. **Canonical-ID succession.** `ApplyCanonicalIDSuccessions` re-keyed operator state with `UPDATE OR IGNORE`, so an existing successor row, even an unlocked one, won and the predecessor's locked row was left behind. Each path is proven by a test that failed on `main`: registry-level for the fold, store-level for the succession, both against the SQLite and in-memory stores. ## The fix The lock follows the resource, in the owning layer (`internal/unifiedresources`), through one pure function (`CarryRemediationLock`) and one sharing step over a group of IDs. - **Link.** `AddLink` writes a block held by any member of the linked component to every member, in the link's own transaction. The store cannot see resource types, so it cannot know which side the registry keeps; a chain of links is one component. - **Unlink.** `AddExclusion` shares just before the split, so each member the unlink leaves keeps the lock the merged resource carried. - **Succession.** The block is carried onto a successor row that already exists. The IDs the predecessor's and successor's links joined before the re-key stay one group and share after it. Sharing waits for the whole batch, so a block-only row made for one predecessor cannot take the place of another's whole-row move. With no successor row the predecessor's whole row still moves, as before. - **Upgrade.** A one-shot carry (`migrateRemediationLockCarry`, recorded in a new `resource_store_migrations` table) restores locks that earlier links and already-recorded successions dropped. It follows recorded succession chains, repeats with the links until nothing changes, and only records itself done once it has converged. A failure is logged and retried at the next start; it never keeps the store from opening. It does not run again, so a lock an operator lifts afterwards stays lifted. Decisions worth a look: - **Retired is carried as `NeverAutoRemediate`, not as retired.** Retired also silences alerts and Patrol attention. That is monitoring posture, not a safety property, and copying it would mute a survivor the operator still watches. Only the block crosses; every other setting of an existing row is left alone and a member with no row gets a row carrying the block alone, attributed `system:identity-change`. - **Persisted copies, not a derived lock.** Deriving the lock from folds at read time would show "off" in the operator-state panel while the gate refuses, with no way to lift it short of unlinking. - **`MemoryStore` now memoizes recorded successions like SQLite.** Otherwise the test double re-keys on every re-declared succession and could put back a lock the operator cleared. ## Known limits (stated in the contract, not fixed here) - Sharing happens at identity changes, not on every operator write. A lock written to a linked ID while the registry cannot resolve it (a member absent from the current generation) stays on that ID until the component's next link, unlink or succession. - The copies written on folded members cannot be reached through the resolving operator-state API while the pair is linked. Lifting the lock on the resource the registry lists holds, but a hidden copy shares again at the component's next link, unlink or succession. After an unlink each member can be edited or cleared on its own. - The store cannot see which links the registry declines to fold (availability-owned endpoints, a node and agent pair a newer split separated), so those share too. - Chains declared across batches carry hop by hop; the stores do not reorder a batch. Real producers (pin-driven and record-declared successions) are star-shaped. - Two other canonical-ID changes reach neither a link nor `ApplyCanonicalIDSuccessions` and still do not move operator state: a physical disk re-keyed in place to a machine-scoped ID, and a host whose earlier ID never held an identity pin. Not part of this change. ## Evidence No GitHub issue or demand-ledger entry asks for this (the nearest ledger entry only records that Retired "also prevents remediation"). It is a defect found by a review of the Assistant "Protected guests" setting, not a feature. ## Verification - New proofs fail on `main` and pass here (`registry_test.go`: `TestResourceRegistry_ManualLinkFoldKeepsRemediationLock`, `..._ManualLinkFoldLockLeavesSurvivorSettings`, `TestLinkedComponentSharesRemediationLockThroughLinksAndUnlinks`, `TestSQLiteStoreRestoresLocksEarlierIdentityChangesDropped`, `TestSQLiteStoreRestoresLocksAcrossRecordedSuccessionChains`; `canonical_id_succession_test.go`: `TestCanonicalIDSuccessionCarriesRemediationLockOntoExistingSuccessorRow`, `TestCanonicalIDSuccessionLockReachesLinkedSurvivor`, `TestCanonicalIDSuccessionKeepsLinkedMembersTogether`, `TestSQLiteSuccessionAndUpgradeCarryIgnoreMalformedLinkRows`). - The full `./internal/unifiedresources/...` package passes, as do `internal/actionlifecycle`, `internal/maintenancesentinel`, `internal/api/resourceapi`, and the operator-state / link / succession subsets of `internal/api`, `internal/monitoring` and `internal/ai/tools` (run on pulse-dev). - Mutation controls on pulse-dev, each breaking one site in a scratch copy and failing the test aimed at it: `AddLink` and `AddExclusion` sharing (SQLite and in-memory), the succession's pre-re-key member capture and group sharing (both stores), sharing per item instead of at the end of the batch (both stores), the remap that follows only later re-keys (including the earlier one-global-map version), and the upgrade carry's chain walk, repeat pass and marker. - Backend-only; no frontend files change, so there is no browser pass. - The contract prose in `unified-resources.md` is updated (operator-state contract with its limits, the fold-resolution paragraph, and the succession section). |
||
|
|
7d02f9f04e |
Stop monitoring linking a node to an agent the operator split off
An operator's split of a Proxmox node and its pulse-agent (report-merge or unlink) split the registry's rows in #2741, but the monitor's own link (Node.LinkedAgentID, Host.LinkedNodeID) stayed. Everything that reads that link kept treating two machines as one: the agent's SMART inventory as the node's disk fallback, its --disk-exclude patterns on the node's disks, guest discovery through the agent, deployment reporting the node as already running an agent, service discovery, the agent's container filesystem readings, the node's temperatures and the node/agent alert correlation. The resource store's exclusion is now the one record of the split. MonitorAdapter decides it with the registry's own rule for a node's link and confirms a split against the store's latest decisions, and models.State holds both sides of a split link back on every link write, before a link can steer where node views fold. The agents API's node link is the operator's newer decision, so it removes the pair's split from the store (TakeExclusion) before recording manual intent, and puts it back, unless the operator decided again meanwhile, if the intent fails to persist (RestoreExclusion). A split recorded after a manual link ends that intent on the agent's next report that finds the split confirmed, so the node is no longer reserved for that agent. A rebuild that cannot read the store's decisions still loads none, in the registry as in monitoring; that needs a change in registry.go and monitor_adapter.go and is left for a follow-up. |
||
|
|
d196f1cb51 |
Open the Patrol mode selector when a link lands on its anchor (#2784)
## What was wrong
The "Choose Patrol mode" buttons in Settings > Assistant (header and
Step 1) and the licence panel's Patrol control action link to
`/patrol#patrol-control`. Since the operator workbench redesign
(
|
||
|
|
068893f83f |
Hide section tabs and lift table preview caps in kiosk
Kiosk is the read-only wall display from issue 917. With the filter bars gone (#2779) two things still assumed someone at the keyboard: the platform section rail, which the maintainer's replies on #917 and #1102 say kiosk hides, and the Proxmox nodes preview that stops at twelve rows behind a Show all button. PlatformSectionTabs now stays unmounted in kiosk and createPlatformTablePreview shows every row with no toggle, so both rules live in the shared primitives and every platform page and the Backups view rail follow. The rail mounts fresh when kiosk ends so its phone scroll controls bind to the new element, and the preview keeps its expansion across kiosk so an open node drawer is not stranded. On the 50-node mock estate the rail cost a 1280x400 display 53px, and the cap kept 38 nodes off it, including the offline node ranked 23rd; kiosk hides the navigation with its Alerts count, so the table rows are where a node going down shows. Drawer search fields and the node-scoped Guests heading reset stay, since a click or a chosen URL narrows the page and these are the way back; the contract records why next to the FilterBar kiosk rule and corrects the stale preview row counts (twelve and six). |
||
|
|
6409a38480 |
Explain safe containment after an accidental public disclosure
Existing forms warn against uploading secrets but stop at prevention. Explain issuer revocation, separate attachment removal, surviving copies, private follow-up and independent monitoring coverage without asking for another diagnostic or credential test. Change-source: pulse-maintainer Contract-Neutral: Documentation and guidance coverage only; runtime, API, storage and deployment contracts are unchanged. |
||
|
|
a4cdb4af87 |
Admit human action dispatch under the policy coordinator (#2781)
## What was wrong
`actionlifecycle.Service.Execute` read the operator remediation lock
inside `ValidateCurrentReadiness` and committed the `executing`
transition afterwards, with nothing holding the two together. A "Never
auto-remediate" (or Retired) save that landed between the readiness read
and `RecordActionExecutionAdmission` was acknowledged to the operator
and the approved action still reached the executor. `ExecuteUnderPolicy`
already held the admission read lock across the same steps, and
operator-state PUT/DELETE already take the write side through
`WithPolicyMutation`, so only the human path was outside the boundary.
No downstream gate closed it: the HTTP handler delegates straight to
`Execute`, none of the typed executors (`routedActionExecutor`, Docker,
Proxmox guest, host update, host storage cleanup) read the lock, and
neither store's admission write consults operator state.
## Change
`Execute` now runs its gates and the admission commit in
`beginHumanExecution` under `coordinator.mu.RLock()` and releases the
lock before `dispatchCommitted` calls the executor. A lock save either
lands before the readiness check and refuses the action, or queues
behind the committed admission.
An admitted dispatch is deliberately not recalled by a later lock,
including an attempt that was admitted but never sent and is resumed by
`RecoverExecutingActions`. Recalling known-unsent work needs an atomic,
conditional pre-send terminalization that neither store has
(`RecordActionExecutionRefusal` only accepts planned/pending/approved
rows; `ForceFail` records inconclusive truth). The contract now says so,
and notes that operator-facing copy should say an already-admitted
action can still run.
## Proof
-
`TestExecuteAdmissionLinearizesWithOperatorLockSave{MemoryStore,SQLite}`
starts a real policy writer inside the window (through the executor's
availability hook, after the lock read and before admission), asserts
the coordinator excludes a writer there, and orders the save
acknowledgement against the admission commit. Against the old
`service.go` it fails on both assertions: ack seq 1, admission seq 2,
executor called.
- `TestExecuteAdmittedDispatchIsNotRecalledByLaterOperatorLock` pins
release-before-dispatch (a save during `ExecuteAction` must not block)
and no recall.
-
`TestRecoverExecutingActionsDoesNotRecallQueuedAttemptAfterOperatorLock`
pins the unsent-attempt boundary.
- The whole `internal/actionlifecycle` package passes under `-race` on
pulse-dev.
Hooks now fire under the read lock on the human path as they already did
on the automatic path. `WithPolicyMutation`'s comment and
`api-contracts.md` say hooks must not request a policy write or re-enter
`Execute`/`ExecuteUnderPolicy`.
## Not changed
`PulseToolExecutor.recordActionExecutionStart`
(`internal/ai/tools/action_audit.go`) has the same check-then-record
shape outside the coordinator. I did not find a live route into it: its
mutation helpers (`executeRunCommand`, `executeControlGuest`,
`executeDockerControl`, `executeKubernetesScale`,
`executeNativeAppContainerControl`) have no non-test callers, and the
file write/append helpers sit behind `pulse_file_edit`, which the
mutation registry records as `retired_denied` /
`denied_before_transport`. Removing that retained code belongs with the
dead Assistant handler cleanup.
|
||
|
|
e7d8092c63 |
Reject case-variant guest filesystem identity fields
Apply the existing canonical-key admission rule to filesystem identity and filter metadata as well as counters. A case-variant mount or type can otherwise overwrite a real volume, filter it out and falsely publish a complete healthy inventory. Preserve valid peers with the existing completeness error and hold only the original qualified disk evidence. Cover metadata ordering, array and object replies, both collectors, History, alert continuity and genuine ordinary recovery without changing guest command admission. Contract-Neutral: Repairs existing filesystem identity and incomplete-reading invariants; no public wire schema, guest command, retry, lock, permission, threshold or backup contract delta. Change-source: pulse-maintainer |
||
|
|
c97ae3f661 |
Stop Storage group headers pinning the group they collapse (#2782)
## Why On Proxmox > Storage grouped by node, clicking a group header did two things at once: it toggled the group and pinned it. The pin wrote `summaryGroup=storage:node:<name>` into the URL, showed **Clear selection**, tinted the group's pool rows, and closed any open pool in another group. Because the same click also collapsed an open group, the tinted rows were hidden in the common case. The chevron on the same row only toggled, so the row and its own chevron behaved differently. The pin was built to rescope the Storage summary charts, which were deleted with the standalone surfaces. Nothing else read it. The header hover preview, which tinted member rows, only fed those charts too. The demand ledger has no entry for group hover or pin, and no GitHub issue asks for either. #2723 removed the Workloads equivalent for the same reason. ## What changes - `StorageGroupRow`: a header click opens or closes the group, the same as the chevron. No hover preview, no pin, no `data-summary-row-active` or `data-summary-group-series-count`. It keeps `data-summary-group-id`, so the shared clear-surface click still leaves an open pool alone. - `useStoragePageModel`, `Storage`, `StorageContentCard`, `StoragePoolsTable`, `StoragePoolRow`: remove the hovered and focused group scope, `summaryGroup` hydration and writing, the effects that kept them consistent, the `setExpandedPoolId` wrapper whose only job was clearing the group pin, and the pool-row `data-summary-group-member-active` state. **Clear selection** now appears only for an open pool or a selected disk. - `storageSummaryGroups.ts` keeps only `buildStorageSummaryGroupId`. - Shared helpers: Storage was the last surface passing a group scope, so `summaryCardInteraction.ts` drops `SummarySeriesGroupScope` and the group resolvers, and `summaryTableFocus.ts` drops `focusedGroupId`, the hovered and focused group scope options, `activeGroupScope` and the pinned-group-header scroll. - `index.css`: removes the `data-summary-group-member-active` rules and their color tokens. - `routing/resourceLinks.ts`: storage route state no longer names `summaryGroup`. No link builder ever passed one; a leftover param in a saved URL is ignored. - Contracts: frontend-primitives rules 8, 10 and 11; storage-recovery clauses 17 and 20 and the Storage contextual-focus paragraph; one sentence in unified-resources' storage route-state paragraph. ## Verification - Exercised the live page on a private mock stack (backend and Vite built from this tree's base, Vite serving the change) at 1440x900 and 390x844: - Before: one click on the `cluster` header collapsed it (20 to 18 rows) and added `summaryGroup`. **Clear selection** appeared but no pinned rows were visible. With `offsite-vault` open in another group, the same click closed it. - After: the click only collapses and expands, and the URL and **Clear selection** are unchanged. An open pool in another group stays open, and Escape still clears it. A hand-typed `?summaryGroup=` URL is ignored. `?resource=ceph-rbd` still reopens its collapsed group and opens the pool. Pool-row hover emphasis is unchanged. A phone tap on a header toggles the group. - Receipt: `frontend-modern/browser-verification/7aba289965a8c192.json`. - Typecheck, eslint and prettier on the changed files, plus the theme, copy, form-label and table-row audits, passed on pulse-dev against the original base. The commit was then replayed onto current main, after 880 more commits, none of which touches Storage, the summary focus helpers, `resourceLinks.ts` or `index.css`; the only same-file overlaps were three contract docs and `SharedPrimitives.guardrails.test.ts`, which applied cleanly. On that tree typecheck passes and 7,675 tests pass across the Storage, shared, routing, utils, platformPage and `src/__tests__` suites plus the three Workloads files that name `summaryGroup`. - One file in that run, `shippedDocFragments.test.ts`, hit its hard-coded 15s `beforeAll` timeout while pulse-dev sat at load 40-70. Alone it passes 3 of 3 runs on this tree and 3 of 3 on main. - An earlier full run on the original base had 33 failures, all 5s timeouts or workers that never started at load 110-140. Those files were re-run with one worker and passed, except two `App.architecture.test.ts` cases that read `tests/integration` files that run's frontend-only tree didn't include (`ENOENT`); they pass in the full clone. - Negative controls: the three rewritten Storage tests and the rewritten `StorageGroupRow` test fail against main's runtime source. - Commit hook battery and governance audits pass. - Second-opinion review over two rounds. Round 1 found no functional regression or remaining consumer, and one overstated sentence in storage-recovery clause 17, now fixed. Round 2 confirmed the receipt's content hashes and claims, and narrowed two historical sentences that still overstated what the old pin did. |
||
|
|
6223788b6c |
Incorporate batch-start upstream frontier with reviewed filesystem assertions
Preserve the exact reviewed Core candidate and existing canonical integrations. Incorporate only the locally supplied batch-start origin/main frontier; publication remains with the scheduled gate. Change-source: pulse-maintainer |
||
|
|
395e9016b2 | Merge origin/main into fix/operator-lock-survives-identity-change | ||
|
|
b2eed76e36 |
Open the Patrol mode selector when a link lands on its anchor
The "Choose Patrol mode" buttons in Settings > Assistant and the licence
panel's Patrol control action link to /patrol#patrol-control. Since the
operator workbench redesign (
|
||
|
|
d70c60e5c4 |
Settle succession lock groups in application order
The sharing groups a succession batch saves were mapped through the batch's re-keys with one map, ignoring the order they applied in. A batch declared against its direction collapsed its groups to singletons and left the surviving ID unlocked, and a batch that returned an ID to its start gave a row to the ID it had just left. A saved group now follows only the re-keys applied after it, in order. The contract also says unlinking frees a member to be edited on its own only once no other link still joins it to the resource. |