diff --git a/docs/API.md b/docs/API.md index e57e3101f..364eb0a26 100644 --- a/docs/API.md +++ b/docs/API.md @@ -115,6 +115,14 @@ Note: `GET /api/resources` is optimized for list views. Some large, platform-spe Note: guest disk usage percentages use `-1` as an "unknown" sentinel — reported when a VM is stopped or its guest agent is unavailable, so there is no filesystem view to measure. Consumers should treat negative values as "no data", not as a percentage; the accompanying `diskStatusReason` field (e.g. `vm-stopped`, `agent-disabled`) says why. +Availability is an additive resource facet. `availability` is the compatibility +summary used by existing clients; `availabilityChecks` contains every check +attached to the resource. Each check can include `correlationState` +(`attached`, `standalone`, `ambiguous`, or `unresolved`), its correlation +rule/reason/candidate count, and an `evidence` envelope with observation and +validity timestamps. Attached targets also add a `checks` relationship and do +not appear as separate `network-endpoint` rows. + `GET /api/resources/stats` Returns aggregations (counts + health rollups). @@ -1325,6 +1333,11 @@ Target payload fields: - `failureThreshold` - Number of consecutive failures before alerting; defaults to 2. - `linkedResourceId` - Optional resource id hint for attaching the probe facet to an existing resource. +An explicit `linkedResourceId` is authoritative and fails closed when it +cannot resolve. Without it, Pulse attaches only on one exact normalized IP or +hostname match. Zero matches remain standalone and multiple matches remain +ambiguous; Pulse does not guess. + Example ping-only target: ```json diff --git a/docs/UPGRADE_v6.md b/docs/UPGRADE_v6.md index 8490aa0fa..74eee91e3 100644 --- a/docs/UPGRADE_v6.md +++ b/docs/UPGRADE_v6.md @@ -185,6 +185,15 @@ Unified Resources is now the canonical model and endpoint family: - Canonical: `/api/resources` +Availability checks now attach to an existing canonical resource when an +explicit `linkedResourceId` resolves or one normalized IP/hostname match is +unambiguous. Attached checks disappear from the standalone Availability checks +inventory and appear on the owning platform row/detail instead. API consumers +should accept the additive `availabilityChecks`, correlation, evidence, and +`checks` relationship fields; the existing singular `availability` field +remains as a compatibility summary. Ambiguous or invalid links stay +standalone/unresolved and are never guessed. + ### License and Entitlements Pulse v6 feature gating is driven by the entitlements endpoint: diff --git a/docs/release-control/v6/internal/OPERATIONAL_TRUST_IMPLEMENTATION_SPEC.md b/docs/release-control/v6/internal/OPERATIONAL_TRUST_IMPLEMENTATION_SPEC.md index 8b4a8f8d5..da5d3cc46 100644 --- a/docs/release-control/v6/internal/OPERATIONAL_TRUST_IMPLEMENTATION_SPEC.md +++ b/docs/release-control/v6/internal/OPERATIONAL_TRUST_IMPLEMENTATION_SPEC.md @@ -1,12 +1,12 @@ # Pulse v6 Operational Trust Implementation Spec -Last updated: 2026-07-18 +Last updated: 2026-07-19 Status: ACTIVE Primary governance surface: - `status.json.candidate_lanes.protection-posture-attention-queue` -Related governed surface: -- `status.json.candidate_lanes.availability-as-resource-facet` +Resolved related governed surface: +- `internal/records/operational-trust-availability-resource-facet-2026-07-19.md` ## Intent @@ -889,9 +889,10 @@ Primary candidate: - `protection-posture-attention-queue` -Sequential related candidate: +Resolved sequential related candidate: -- `availability-as-resource-facet` +- `availability-as-resource-facet`, closed by + `internal/records/operational-trust-availability-resource-facet-2026-07-19.md` The primary candidate's subsystem mapping must include the canonical owners that this implementation touches, including alerts, notifications, storage and diff --git a/docs/release-control/v6/internal/records/operational-trust-availability-resource-facet-2026-07-19.md b/docs/release-control/v6/internal/records/operational-trust-availability-resource-facet-2026-07-19.md new file mode 100644 index 000000000..7caf87bff --- /dev/null +++ b/docs/release-control/v6/internal/records/operational-trust-availability-resource-facet-2026-07-19.md @@ -0,0 +1,160 @@ +# Operational Trust: Availability as a Resource Facet + +Date: 2026-07-19 + +## Decision + +Availability is evidence about a canonical resource, not a parallel inventory +taxonomy. + +Pulse attaches a saved check in this order: + +1. an explicit canonical resource link, which is authoritative and fails closed +2. one exact normalized IP address match +3. one exact normalized hostname match + +Zero automatic matches remain standalone. Multiple matches are ambiguous. +Invalid explicit links are unresolved and do not fall back to address matching. +Availability-owned endpoints never become attachment candidates. + +Every attached check is retained in `availabilityChecks`; the singular +`availability` field remains an additive compatibility summary. Each attached +target emits a `checks` relationship and a canonical evidence envelope bound to +the owning resource. A second check attached to the same resource remains on +that resource and does not reappear as duplicate standalone inventory. + +## Runtime Result + +- `internal/monitoring/availability_poller.go` authors freshness-bounded + operational-trust evidence. A never-observed target is partial/unknown. +- `internal/unifiedresources/availability.go` owns plural facet normalization, + compatibility-summary selection, and exact target lookup. +- `internal/unifiedresources/registry.go` owns explicit, unique address, and + unique hostname correlation plus typed attached/standalone/ambiguous/ + unresolved outcomes. +- Attached evidence is rebound to the canonical subject and records the exact + identity-correlation rule. +- `internal/alerts/unified_incidents.go` routes availability incidents on any + resource type through the canonical alert lifecycle and selects evidence by + the incident's exact target id. +- REST, websocket, workload, Docker, and Standalone projections preserve the + additive plural/correlation/evidence fields without a per-row fetch. +- The owning platform row keeps one compact summary. Workload and Docker detail + surfaces render protocol, complete target, latest result, latency, freshness, + and last observation for every attached check. +- Expired successful evidence renders `Stale`, never `Up` or `Responding + normally`; an unobserved check renders `Not checked`. +- Attached resources are excluded from the standalone Availability checks + inventory. + +## User Lens + +User job: “Tell me whether this machine or service is reachable, on the resource +I already know, and show me when that answer stopped being trustworthy.” + +Live exercise: + +- Opened Docker Overview in the authenticated product. +- The `Tower` host row showed one compact `TCP` availability facet. +- Expanded the deepest host detail. +- The detail showed `Availability`, target `192.168.0.8:8007`, method + `TCP 8007`, result `Up`, latency `1ms`, checked age, and `fresh`. +- Opened Machines > Availability checks. +- The attached `Tower` host was absent; only the two genuinely standalone local + targets remained. + +Distance to answer is one platform navigation plus one row expansion. Every +default-row element is actionable: the compact facet signals whether to open +detail; protocol, target, result, freshness, and observation time explain what +was tested and whether it can still be trusted. Provider-forensic correlation +reason and evidence stay in detail rather than widening the row. + +Keep / demote / cut: + +- Keep one compact availability summary on the owning row. +- Keep complete current-state and freshness detail in the expansion. +- Demote correlation/evidence forensics to detail and API payloads. +- Cut attached targets from standalone primary inventory. +- Cut green reassurance for stale successful observations. +- Cut guessed correlation and endpoint-only lifecycle forks. + +## User Evidence + +- [#1460: Simple ping-based monitoring](https://github.com/rcourtman/Pulse/issues/1460) + asks Pulse to monitor devices that cannot run an agent or SSH. +- [#1565: UDP/service availability without an agent](https://github.com/rcourtman/Pulse/issues/1565) + describes the burden of maintaining a separate `nmap` plus email script. +- [#1568: not all availability checks are shown](https://github.com/rcourtman/Pulse/issues/1568) + demonstrates that missing or duplicate check inventory is a trust defect. +- [#1582: failure threshold timing mismatch](https://github.com/rcourtman/Pulse/issues/1582) + demonstrates that observation timing and freshness must be explicit. +- [Discussion #1508: crashed VM remains green without an agent](https://github.com/rcourtman/Pulse/discussions/1508) + asks for reachability loss to affect the existing VM rather than require a + separate monitoring tool. +- [#1519: clock drift creates stale/offline loops](https://github.com/rcourtman/Pulse/issues/1519) + reinforces the separation between fresh receipt evidence and stale state. + +No public report explicitly requested merging an availability target into a +VM/container row. The canonical decision is therefore an inference from the +resource-coherence and missing-check evidence above, not a claimed direct user +quote. + +## Comparative Evidence + +- [Checkmk host/service model](https://docs.checkmk.com/latest/en/monitoring_basics.html) + treats checks as services of a host and distinguishes unknown, pending, and + stale from down. +- [Zabbix host availability](https://www.zabbix.com/documentation/current/en/manual/web_interface/frontend_sections/data_collection/hosts) + attaches availability to host interfaces and preserves available, + unavailable, mixed, and unknown states. +- [Uptime Kuma](https://github.com/louislam/uptime-kuma) centers independent + monitor objects and keeps pending/maintenance distinct from down. +- [Grafana Synthetic Monitoring checks](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/create-checks/checks/) + centers standalone checks and uses labels for correlation. +- [Grafana missing-data behavior](https://grafana.com/docs/grafana/latest/alerting/guides/missing-data/) + preserves No Data, Error, and MissingSeries rather than silently resolving. + +Pulse follows the attached host/resource facet pattern because it already owns a +canonical cross-platform resource model. It retains standalone endpoints only +where no canonical owner exists, while preserving the shared industry rule that +missing, stale, pending, and unknown evidence are not healthy. + +## Proof + +Focused backend proof covers: + +- explicit link precedence and fail-closed invalid links +- exact IP and hostname attachment +- ambiguous candidate rejection +- plural checks on one canonical resource +- `checks` relationships +- evidence validation, canonical rebinding, freshness, and pre-first-probe + partial/unknown state +- attached Docker service failure through the canonical operational lifecycle +- mock graph attachment without standalone duplication + +Focused frontend proof covers: + +- row and detail presentation +- plural attached cards +- stale-success and unobserved truthfulness +- standalone duplicate exclusion +- freshness-aware status/filter behavior +- shared primitive and no-per-row-fetch guardrails + +Deterministic browser proof: + +- `tests/integration/tests/92-operational-trust-availability-facet.spec.ts` + +Live browser proof was performed against the current authenticated development +runtime after the implementation build, including the Docker row, expanded host +detail, and Standalone Availability checks inventory described above. + +## Governance + +- Candidate lane: `availability-as-resource-facet` +- Owning lanes: L8 and L13 +- Owning contracts: monitoring, unified resources, alerts, API contracts, + frontend primitives, performance and scalability, Patrol intelligence +- This record is the durable evidence for resolving and removing the candidate + and its completed availability coverage gap. diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index ec57782a9..1d8e456fc 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -8813,41 +8813,6 @@ "kind": "file" } ] - }, - { - "id": "availability-as-resource-facet", - "summary": "Agentless availability checks always mint a standalone network-endpoint resource and never attach to a known resource, so a check that monitors a Proxmox guest, Docker container, or Kubernetes workload appears as a disconnected duplicate on the Machines page instead of on the resource it actually monitors. The performance-and-scalability contract already requires agentless availability evidence to belong on the resource's bounded row path, but the ingest path has no cross-source attach: resolveLinkedResource only handles Proxmox<->agent links and findMatch is restricted to agent and physical_disk types, so SourceAvailability records always take the source-specific network-endpoint ID and the Availability facet never reaches an existing resource. The gap is the missing unambiguous attach (by explicit resource link or unique address/hostname correlation) plus platform-row surfacing of the attached facet.", - "owner": "project-owner", - "status": "planned", - "recorded_at": "2026-06-26", - "lane_ids": [ - "L8", - "L13" - ], - "subsystem_ids": [ - "frontend-primitives", - "monitoring", - "unified-resources" - ], - "proposed_resolution": "lane-expansion", - "coverage_impact": 7, - "evidence": [ - { - "repo": "pulse", - "path": "docs/release-control/v6/internal/subsystems/performance-and-scalability.md", - "kind": "file" - }, - { - "repo": "pulse", - "path": "internal/monitoring/availability_poller.go", - "kind": "file" - }, - { - "repo": "pulse", - "path": "internal/unifiedresources/registry.go", - "kind": "file" - } - ] } ], "candidate_lanes": [ @@ -8905,43 +8870,9 @@ "storage-recovery", "unified-resources" ] - }, - { - "id": "availability-as-resource-facet", - "name": "Availability as Resource Facet", - "summary": "Promote availability-as-resource-facet into a governed lane expansion: extend unified-resource ingest so an availability check attaches as a facet on the known resource it monitors (explicit resource link first, unambiguous unique address/hostname correlation second, standalone network-endpoint fallback for genuinely unowned endpoints), keep the ingest on the existing unified-resource hot path with no per-row fetch, and surface the attached Availability facet on the platform resource row as a compact inline target/result readout with the protocol identity badge in the System column, per the performance-and-scalability bounded-row contract.", - "status": "planned", - "recorded_at": "2026-06-26", - "target_id": "v6-product-lane-expansion", - "current_lane_ids": [ - "L8", - "L13" - ], - "coverage_gap_ids": [ - "availability-as-resource-facet" - ], - "subsystem_ids": [ - "frontend-primitives", - "monitoring", - "unified-resources" - ] - } - ], - "work_claims": [ - { - "id": "codex-operational-trust-candidate-lane-protection-posture-attention-queue", - "agent_id": "codex-operational-trust", - "summary": "Canonical operational trust and protection evidence implementation specification", - "target_id": "v6-product-lane-expansion", - "claimed_at": "2026-07-18T21:46:08Z", - "heartbeat_at": "2026-07-18T21:46:08Z", - "expires_at": "2026-07-19T05:46:08Z", - "work_item": { - "kind": "candidate-lane", - "id": "protection-posture-attention-queue" - } } ], + "work_claims": [], "open_decisions": [], "source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", "resolved_decisions": [ diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 7dfc0afc2..e95d7345a 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -1484,6 +1484,11 @@ the intentionally sparse public response. `/api/resources/dashboard-summary` as a compatibility read; lifecycle surfaces must continue to use install inventory, enrollment proof, and fleet freshness truth from their owning contracts. + Those routes may now seed the availability provider from canonical plural + `availabilityChecks` facets so attached checks survive startup + rehydration. Lifecycle surfaces must not reinterpret an attached check, + its compatibility `availability` summary, or its evidence freshness as + agent enrollment, heartbeat, command reachability, or fleet liveness. The same presentation-only boundary now covers compact storage summary chart reads as well. Shared `/api/charts/storage-summary` transport may request only the canonical `used` and `avail` storage series needed for the diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 2032b69bc..0662a477a 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -377,10 +377,16 @@ changes must not reintroduce raw `filepath.Join(dataDir, ...)` joins from caller-supplied directories or ad hoc history filenames. Agentless availability incidents now enter alerts through the same unified resource incident bridge as storage, PBS, VM, and host resource incidents. -`network-endpoint` resources with `SourceAvailability` incidents must create -canonical `resource-incident` alerts with provider display `Availability`; -availability alerting must not introduce a second endpoint-only evaluator or -alert identity family outside `internal/alerts/unified_incidents.go`. +Standalone `network-endpoint` resources and any canonical resource carrying an +attached availability facet must create canonical `resource-incident` alerts +with provider display `Availability`; availability alerting must not introduce +a second endpoint-only evaluator or alert identity family outside +`internal/alerts/unified_incidents.go`. When a resource carries multiple +checks, the incident `NativeID` selects the exact check evidence envelope that +is copied into the alert and its `OperationalRecord`; the singular +compatibility summary must never substitute evidence from a different target. +The same lifecycle transition then projects into Patrol like every other +canonical operational record. Notification transport, provider delivery, queue safety, and notification API transport now live under the explicit `notifications` subsystem inside the diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 911773cf6..bd9208559 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -7315,6 +7315,15 @@ Mock availability fixtures must still behave like saved targets: `/api/connectio reports them as availability rows, `/api/availability-targets` lists them with probe status, and saved-test calls return the synthetic probe result instead of attempting live network I/O against demo-only addresses. +Unified-resource transport adds typed availability trust fields without +changing the saved-target CRUD owner. `availability` remains the singular +compatibility summary; `availabilityChecks` is the complete attached set; each +entry may carry `correlationState`, `correlationRule`, +`correlationReason`, `correlationCandidates`, and an operational-trust +`evidence` envelope. Attached resources also expose one `checks` relationship +per saved target. REST, websocket, mock, and workload projections must preserve +those additive fields unchanged, and frontend consumers must not reconstruct +correlation or evidence from `/api/availability-targets`. That same shared metrics-history contract now also owns physical-disk live I/O windows. `internal/api/router.go` must accept `resourceType=disk` on `/api/metrics-store/history`, keep `30m` as a valid compact live range, and @@ -7786,6 +7795,16 @@ This is required for qualification clients and local operators to read, approve, reject, and execute governed actions without weakening the same capability checks for sessions, proxy users, or API tokens. +### Unified Agent observer report boundary + +Existing host, Docker, and Kubernetes report endpoints accept observer reports +under the observer instance's own API token without a new wire payload shape. +Authority is an agent-side topology property: only the configured primary +destination may have its report response interpreted as configuration or +commands. Proxmox auto-registration remains one destination per request and +must answer a check-registration request before the agent mutates a local PVE +or PBS token. + ### Protection posture transport `GET /api/recovery/postures` is the authenticated `monitoring:read` transport diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index dad5e2696..3c8cf3b51 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -5006,6 +5006,16 @@ and latest latency or failure result once, inline in the agentless endpoint's metric slot, while keeping recent check timing and fuller failure context in the tooltip or drawer so operators can understand what was measured without duplicated row chrome. +Known platform resources use that same compact presentation when availability +is attached. `AvailabilityProbeStatusCard` is the shared detail primitive for +Workloads and Docker host drawers; it renders the complete target, protocol, +latest result, latency when relevant, evidence freshness, and last observation. +Plural attached checks render as repeated bounded cards from +`availabilityChecks`, while the row keeps one compatibility summary. Expired +successful evidence must render an amber `Stale` state with no green +`Responding normally` copy, and a never-observed check must render +`Not checked`. A resource whose correlation state is `attached` must not also +appear as a primary row in the Machines `Availability checks` tab. Operational navigation for those agentless endpoints belongs to the frontend-primitives-owned Machines surface as a focused Availability checks tab rather than a new primary nav item. The page may show availability checks beside diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index e046447fe..2e37b9eab 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -604,7 +604,14 @@ Supplemental records carry the saved target's optional `LinkedResourceID` forward into `AvailabilityData` so the unified-resource registry can attach the probe facet onto the referenced resource. Monitoring does not perform the attach decision itself; it only forwards the link hint for the registry to -resolve. +resolve. Every completed probe also authors an operational-trust +`EvidenceEnvelope` with provider `availability`, collector +`availability-poller`, the saved target as its provider reference, the exact +observation/ingest times, and a validity window of twice the effective polling +interval. Before the first completed probe, evidence is explicitly partial and +unknown with reason `availability_not_observed`; monitoring must never encode +that state as a confirmed failure or a healthy observation. The registry owns +rebinding the envelope subject to a canonical resource after correlation. Availability target kind is monitoring-owned runtime metadata, not a frontend guess. Saved targets carry the bounded `targetKind` values `machine`, `service`, and `device`; monitoring must preserve that value in probe status, supplemental @@ -1735,6 +1742,15 @@ transition detection consumes that status instead of inventing a fixed stale window: a stopped guest can have fresh inventory, while a stale source cannot authoritatively prove either a stopped transition or recovery. +### Unified Agent destination delivery metrics + +The local agent health listener exports +`pulse_agent_destination_configured{module,destination,role}` and +`pulse_agent_destination_delivery_up{module,destination,role}`. Role is bounded +to `primary` or `observer`; destination names come from validated configuration. +Observer delivery failure is visible but does not make the primary authority +unready or merge observer retry state into primary delivery health. + ### PBS protection evidence collection Direct PBS backup enumeration emits two separate storage/recovery inputs: diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 489f17738..181d8b916 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -959,6 +959,15 @@ contextual Assistant handoff. Assistant is absent before selection and receives explanation-only typed context without action or approval authority. Browser proof is `tests/integration/tests/91-operational-trust-attention-workbench.spec.ts`. +Attached availability failures use this same queue and detail contract. Their +attention item keeps the owning canonical resource ID, the exact +availability-poller evidence ID, freshness, impact, and resource deep link; +Patrol must not create an endpoint-only finding family or route the operator to +a duplicate standalone check. A disconnected or expired availability +observation remains stale or unknown and must not be presented as recovered. +The attached-facet browser proof is +`tests/integration/tests/92-operational-trust-availability-facet.spec.ts`. + ## Current State The active Patrol queue now uses compact severity-accented rows for diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index d1e7b15fc..c041e5608 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1614,6 +1614,13 @@ result text across the resource identity cell and metric cells. That text must derive from the existing resource payload and shared presentation helper instead of adding a per-row fetch, extra hydration pass, or unbounded badge stack. +The canonical payload may carry multiple attached checks in +`availabilityChecks`; `availability` remains the bounded compatibility summary +selected from that already-hydrated set. Resource rows render one compact +summary and detail surfaces iterate the bounded attached set from the same +payload. Neither plural presentation nor freshness evaluation may issue a +check-specific request, reconstruct the set from the settings API, or add a +second websocket hydration path. The infrastructure summary hot path is now explicit shared ownership too: `InfrastructureSummary.tsx` stays a render shell, `useInfrastructureSummaryState.ts` owns chart polling and cache lifecycle, and diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 29808d3b5..9348833ce 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -5212,6 +5212,7 @@ "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ + "internal/monitoring/availability_poller_test.go", "internal/monitoring/canonical_guardrails_test.go", "internal/monitoring/monitor_backups_readstate_test.go", "internal/monitoring/monitor_host_agents_test.go", @@ -5732,6 +5733,7 @@ "frontend-modern/src/components/Workloads/workloadUrlSyncModel.ts", "frontend-modern/src/hooks/useWorkloads.ts", "frontend-modern/src/routing/routePreload.ts", + "frontend-modern/src/types/workloads.ts", "frontend-modern/src/useAppRuntimeState.ts", "frontend-modern/src/utils/thresholdSliderPresentation.ts", "frontend-modern/src/utils/workloadsSummaryCache.ts", @@ -5873,6 +5875,7 @@ "frontend-modern/src/components/Workloads/workloadTopology.ts", "frontend-modern/src/components/Workloads/workloadUrlSyncModel.ts", "frontend-modern/src/hooks/useWorkloads.ts", + "frontend-modern/src/types/workloads.ts", "frontend-modern/src/utils/thresholdSliderPresentation.ts", "frontend-modern/src/utils/workloadsSummaryCache.ts" ], @@ -5881,6 +5884,7 @@ "exact_files": [ "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx", "frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts", + "frontend-modern/src/components/Workloads/__tests__/AvailabilityProbeStatusCard.test.tsx", "frontend-modern/src/components/Workloads/__tests__/DiskList.test.tsx", "frontend-modern/src/components/Workloads/__tests__/EnhancedCPUBar.test.tsx", "frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx", @@ -6498,6 +6502,7 @@ "frontend-modern/src/components/Discovery/DiscoveryTab.tsx", "frontend-modern/src/components/Discovery/useDiscoveryTabState.ts", "frontend-modern/src/components/Docker/SwarmServicesDrawer.tsx", + "frontend-modern/src/components/Infrastructure/AvailabilityProbeStatusCard.tsx", "frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts", "frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx", "frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx", @@ -6535,6 +6540,7 @@ "frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx", "frontend-modern/src/features/docker/DockerContainersTable.tsx", "frontend-modern/src/features/docker/dockerContainerTableModel.ts", + "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx", "frontend-modern/src/features/docker/DockerHostsTable.tsx", "frontend-modern/src/features/docker/DockerImagesTable.tsx", "frontend-modern/src/features/docker/DockerNativeTableShared.tsx", @@ -6596,6 +6602,7 @@ "frontend-modern/src/types/resource.ts", "frontend-modern/src/utils/actionAuditPresentation.ts", "frontend-modern/src/utils/agentResources.ts", + "frontend-modern/src/utils/availabilityProbePresentation.ts", "frontend-modern/src/utils/canonicalResourceTypes.ts", "frontend-modern/src/utils/platformSupportManifest.generated.ts", "frontend-modern/src/utils/resourceBadgePresentation.ts", @@ -6678,6 +6685,7 @@ "frontend-modern/src/components/Discovery/DiscoveryTab.tsx", "frontend-modern/src/components/Discovery/useDiscoveryTabState.ts", "frontend-modern/src/components/Docker/SwarmServicesDrawer.tsx", + "frontend-modern/src/components/Infrastructure/AvailabilityProbeStatusCard.tsx", "frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts", "frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx", "frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx", @@ -6715,6 +6723,7 @@ "frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx", "frontend-modern/src/features/docker/DockerContainersTable.tsx", "frontend-modern/src/features/docker/dockerContainerTableModel.ts", + "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx", "frontend-modern/src/features/docker/DockerHostsTable.tsx", "frontend-modern/src/features/docker/DockerImagesTable.tsx", "frontend-modern/src/features/docker/DockerNativeTableShared.tsx", @@ -6986,6 +6995,29 @@ "frontend-modern/src/utils/__tests__/resourcePolicyPresentation.test.ts" ] }, + { + "id": "availability-facet-presentation", + "label": "availability facet resource presentation proof", + "match_prefixes": [], + "match_files": [ + "frontend-modern/src/components/Infrastructure/AvailabilityProbeStatusCard.tsx", + "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx", + "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx", + "frontend-modern/src/features/standalone/standalonePageModel.ts", + "frontend-modern/src/features/standalone/StandalonePageSurface.tsx", + "frontend-modern/src/utils/availabilityProbePresentation.ts" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "frontend-modern/src/components/Workloads/__tests__/AvailabilityProbeStatusCard.test.tsx", + "frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx", + "frontend-modern/src/features/standalone/__tests__/AvailabilityChecksTable.test.tsx", + "frontend-modern/src/features/standalone/__tests__/standalonePageModel.test.ts", + "frontend-modern/src/utils/__tests__/availabilityProbePresentation.test.ts", + "tests/integration/tests/92-operational-trust-availability-facet.spec.ts" + ] + }, { "id": "metrics-target-runtime", "label": "unified resource metrics target proof", @@ -7017,6 +7049,7 @@ "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ + "internal/unifiedresources/availability_link_test.go", "internal/unifiedresources/kubernetes_registry_test.go", "internal/unifiedresources/pbs_pmg_registry_test.go", "internal/unifiedresources/registry_merge_policy_test.go", @@ -7064,7 +7097,9 @@ "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ + "internal/unifiedresources/availability_link_test.go", "internal/unifiedresources/canonical_id_pins_test.go", + "internal/unifiedresources/clone_test.go", "internal/unifiedresources/code_standards_test.go", "internal/unifiedresources/kubernetes_registry_test.go", "internal/unifiedresources/metrics_targets_test.go", diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 8d3d7fa72..ffb97cccf 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -1290,6 +1290,12 @@ recovery scope, or a storage/recovery-owned secret source. for presentation, while storage and recovery must continue to treat `AgentData.platform` as the normalized runtime platform. 32. Keep agentless availability endpoints neutral on the shared unified-resource and API contracts. When `internal/api/availability_handlers.go`, `internal/api/connections_handlers.go`, `internal/api/platform_mock_connections.go`, or `frontend-modern/src/hooks/useUnifiedResources.ts` surface `network-endpoint` availability resources, storage and recovery may consume their liveness as infrastructure context only; they must not reinterpret ping/TCP/HTTP endpoints as storage providers, backup targets, recovery repositories, or protected-workload evidence. + The same neutrality applies when one or more canonical + `availabilityChecks` facets attach to a storage-related resource. The + additive plural facet, its compatibility `availability` summary, evidence + freshness, and `checks` relationship remain monitoring context; they do not + mint a second storage row or become backup coverage, recovery readiness, or + restore evidence. That neutrality includes availability targets whose `targetKind` is `machine`. A Linux server, desktop, laptop, or Mac mini monitored by an agentless reachability check still belongs to Availability checks rather diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 3b81f7f2c..d341e1cae 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -255,16 +255,16 @@ Alert decoration on those platform rows consumes the canonical active-alert read model and the detector-enabled accessor. External notification activation is not a resource-health field and must never suppress row alerts, change resource filtering, or create a parallel platform-local alert truth. -The standalone Pulse Agent and Availability monitor may add one compact status -summary immediately above its canonical table. That summary must be derived -from the same already-loaded unified-resource slice, use canonical resource -status and `lastSeen` fields, keep the machine row indicator on that same -freshness-aware presentation so an old agent cannot stay visually green while -the summary warns, keep failed or degraded checks ahead of healthy -checks, and route management back to the canonical infrastructure or -availability settings paths. It must not introduce a page-local fetch, generic -Home dashboard, decorative chart, shadow health model, or proof strip detached -from the table it summarizes. +The standalone Machines monitor keeps resource-specific health on the canonical +table row and resource detail surface rather than duplicating row warnings in a +page-wide posture banner. Its row indicator must use the same canonical resource +status and `lastSeen` fields so an old agent cannot stay visually green. +The Availability monitor may add one compact status summary immediately above +its canonical table. That summary must be derived from the same already-loaded +unified-resource slice, keep failed or degraded checks ahead of healthy checks, +and route management back to the canonical availability settings path. It must +not introduce a page-local fetch, generic Home dashboard, decorative chart, +shadow health model, or proof strip detached from the table it summarizes. Availability timestamps must preserve absence as absence. Monitoring and mock adapters must project zero `LastChecked` or `LastSuccess` values as nil canonical facet pointers so REST and WebSocket consumers render `Not checked` @@ -1745,39 +1745,53 @@ canonical resources expose to alerts, AI, and frontend consumers. Agentless availability checks are now canonical resources rather than connection-only status rows. `SourceAvailability` emits `network-endpoint` records with the saved target id, probe address, protocol, cadence, last check, -failure count, and threshold in `AvailabilityData`. Registry merge policy must -preserve that payload and incident state. An availability record attaches as a -facet onto a known resource when (a) the target carries an explicit -`LinkedResourceID` that resolves to a resource already in the registry by exact -resource id, unique source id, or unique canonical identity alias, or (b) the -probe address unambiguously matches exactly one known resource by IP through -`FindCandidates` with reason `ip` or `hostname+ip` (confidence at or above the -merge threshold). Fuzzy hostname-only correlation (reason `hostname`, confidence -below threshold), ambiguous source/canonical references, and references to -availability-owned resources must not attach. When attached, the known resource -inherits the `AvailabilityData` facet and `SourceAvailability` in its source -list, and no standalone `network-endpoint` is minted. When no link resolves, the -record falls back to a standalone `network-endpoint` as before. A second probe -must not overwrite a facet already attached by a different target; the second -probe stays standalone in that case. +failure count, threshold, correlation outcome, and evidence envelope in +`AvailabilityData`. Registry merge policy must preserve that payload and +incident state. + +Correlation is fail-closed and ordered. A non-empty `LinkedResourceID` is +authoritative: it may resolve by exact canonical resource id, one unique source +id, or one unique canonical identity alias, and an invalid or ambiguous +explicit reference remains `unresolved` without falling back to address +matching. Without an explicit reference, the registry may attach only when one +normalized IP address or one exact normalized hostname matches exactly one +non-availability-owned canonical resource. Zero matches are `standalone`; +multiple matches are `ambiguous`; neither may be guessed. A genuinely +standalone target keeps its `network-endpoint`. An attached target does not +mint a duplicate endpoint. + +The attached resource carries every check in the canonical +`availabilityChecks` facet, keyed by saved target id, while `availability` +remains an additive singular compatibility summary selected from that set. +Adding a second explicit or unambiguously correlated check must retain both +checks on the same resource, emit one `checks` relationship per target, and +must not force the later check into duplicate standalone inventory. Each +attached check's evidence subject is rebound to the owning canonical resource +and includes the exact correlation rule and matched field. Ambiguous and +unresolved standalone evidence carries a typed reason instead. Frontend resource adapters must preserve that same availability identity on both REST and realtime paths: a thin `network-endpoint` update with availability data is still `platformType=availability`, `sourceType=api`, and must not regress to a generic platform badge in infrastructure rows or drawers. Infrastructure row presentation must also consume that availability payload as operator evidence, not only as badge identity. Any resource row carrying an -`AvailabilityData` facet—whether a standalone `network-endpoint` or a known +availability facet—whether a standalone `network-endpoint` or a known guest that inherited the facet through explicit link or IP correlation—must surface one visible probe readout from either the top-level availability field or the live-state `platformData.availability` mirror: the System column uses the probe protocol as the compact identity badge (`ICMP`, `TCP`, or `HTTP`), while the metric cell shows only the target detail and latest latency or failure result, such as `6053: 11 ms`, `/status: 503`, `3 ms`, or `timed out`. +The owning detail renders protocol, full target, latest result, latency, +freshness, and last observation for every attached check. Expired successful +evidence is `stale`, not `Up` or `Responding normally`; an unobserved check is +`Not checked`, not `Down`. Frontend primitives owns Machines as the operational presentation for those same agentless checks; unified resources owns the projection contract consumed there. `StandalonePageSurface.tsx` must fetch both `agent` and `network-endpoint` resources, keep standalone machines and availability checks -as separate buckets in `standalonePageModel.ts`, and let +as separate buckets in `standalonePageModel.ts`, exclude every resource whose +availability correlation state is `attached`, and let `AvailabilityChecksTable.tsx` render saved probe method, target, latest result, check age, failure count, and cadence from the canonical availability payload. Recent check timing and fuller failure context may stay in tooltip or drawer diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index a55d23f60..f291996ec 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -311,6 +311,7 @@ }, "requiredConsumers": [ { "path": "src/components/Discovery/DiscoveryTab.tsx" }, + { "path": "src/components/Infrastructure/AvailabilityProbeStatusCard.tsx" }, { "path": "src/components/Infrastructure/ResourceActionHistory.tsx" }, { "path": "src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx" }, { "path": "src/components/shared/WebInterfaceUrlField.tsx" }, diff --git a/frontend-modern/src/components/Infrastructure/AvailabilityProbeStatusCard.tsx b/frontend-modern/src/components/Infrastructure/AvailabilityProbeStatusCard.tsx new file mode 100644 index 000000000..43cf2eb2a --- /dev/null +++ b/frontend-modern/src/components/Infrastructure/AvailabilityProbeStatusCard.tsx @@ -0,0 +1,170 @@ +import { For, Show, createMemo } from 'solid-js'; +import { Activity, AlertCircle, Check } from 'lucide-solid'; + +import type { ResourceAvailabilityMeta } from '@/types/resource'; +import { InfoCardFrame } from '@/components/shared/InfoCardFrame'; +import { + getAvailabilityProbeMethodLabel, + getAvailabilityProbeEndpointLabel, + getAvailabilityProbePresentation, +} from '@/utils/availabilityProbePresentation'; +import { formatRelativeTime } from '@/utils/format'; + +export interface AvailabilityProbeStatusCardProps { + availability: ResourceAvailabilityMeta; +} + +export interface AvailabilityProbeStatusCardsProps { + availability?: ResourceAvailabilityMeta; + checks?: ResourceAvailabilityMeta[]; +} + +export function AvailabilityProbeStatusCards(props: AvailabilityProbeStatusCardsProps) { + const checks = createMemo(() => { + const byTarget = new Map(); + for (const check of props.checks ?? []) { + const key = + check.targetId?.trim() || + `${check.protocol ?? ''}:${check.address ?? ''}:${check.port ?? ''}:${check.path ?? ''}`; + byTarget.set(key, check); + } + if (props.availability) { + const check = props.availability; + const key = + check.targetId?.trim() || + `${check.protocol ?? ''}:${check.address ?? ''}:${check.port ?? ''}:${check.path ?? ''}`; + if (!byTarget.has(key)) byTarget.set(key, check); + } + return [...byTarget.values()]; + }); + + return ( + + {(availability) => } + + ); +} + +export function AvailabilityProbeStatusCard(props: AvailabilityProbeStatusCardProps) { + const isUp = () => props.availability.available === true; + const isDown = () => props.availability.available === false; + const latency = () => { + const ms = props.availability.latencyMillis; + return typeof ms === 'number' && Number.isFinite(ms) && ms > 0 ? `${Math.round(ms)}ms` : null; + }; + const lastChecked = () => formatRelativeTime(props.availability.lastChecked); + const method = () => getAvailabilityProbeMethodLabel(props.availability); + const presentation = () => + getAvailabilityProbePresentation({ + type: 'network-endpoint', + platformType: 'availability', + status: isUp() ? 'online' : isDown() ? 'offline' : 'unknown', + availability: props.availability, + }); + const isStale = () => presentation()?.freshnessLabel === 'stale'; + const isFreshUp = () => isUp() && !isStale(); + const targetAddr = () => getAvailabilityProbeEndpointLabel(props.availability); + const failureLabel = () => { + const err = (props.availability.lastError ?? '').trim(); + if (!err) return null; + if (/timed?\s*out/i.test(err)) return 'Timed out'; + const httpMatch = err.match(/\b([45]\d{2})\b/); + if (httpMatch) return `HTTP ${httpMatch[1]}`; + if (/refused|unreachable|no route/i.test(err)) return 'Unreachable'; + return err.length > 40 ? `${err.slice(0, 40)}…` : err; + }; + + return ( + +
+
+
+ + {isStale() ? 'Stale' : isUp() ? 'Up' : isDown() ? 'Down' : 'Not checked'} + +
+
+
+ Latency + —} + > + + {latency()} + + +
+
+ Method + + {method()} + +
+
+ Target + + {targetAddr()} + +
+ +
+ Checked + {lastChecked()} +
+
+
+ Freshness + + {presentation()?.freshnessLabel ?? 'freshness unknown'} + +
+ + {(label) => ( +
+ Resource + {label()} +
+ )} +
+ +
+
+
+ +
+
+
+
+
+ ); +} diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx index debee409a..aeea87f6d 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx @@ -594,6 +594,11 @@ export const UnifiedResourceHostTableCard: Component )} + + {probe().freshnessLabel} + )} diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx index 87898487c..26ade20b0 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx @@ -355,9 +355,9 @@ export const AvailabilityTargetSlot: Component = (p updateForm({ linkedResourceId: event.currentTarget.value }) } fieldClass="sm:col-span-2" - help="Link this check to a known resource so its status appears on that resource's row. Leave empty to auto-detect by IP address." + help="Link this check to a known resource so its status appears on that resource's row. Leave empty to attach only when its IP address or full hostname has one exact match." > - +