handle Fleet Node outages as miner unavailability - #995
Conversation
Derive miner availability from Fleet Node heartbeats, show no-connection state consistently, and add node outage and recovery alerting with least-privilege Grafana access.
🔐 Codex Security Review
Review SummaryOverall Risk: MEDIUM Findings[MEDIUM] Persisted uptime ignores Fleet Node outages
[MEDIUM] Rack slots remain healthy while rack counts report offline
NotesThe protobuf change is additive and the changed dynamic SQL remains parameterized. No authentication, injection, credential exposure, or pool-hijacking issue was identified in the reviewed hunks. Generated by Codex Security Review | |
Derive miners behind stale Fleet Nodes as OFFLINE with a typed reason, preserve their stored status for immediate recovery, and keep filters and command selectors aligned. Add generic least-privilege node outage alerts and focused UI, query, permission, and lifecycle coverage.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8e75ce9405
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
🔵 Needs a closer look
It is a cross-cutting change spanning protobuf contracts, SQL query semantics, alerting/provisioning, and UI behavior, so it merits final human review for operational correctness and performance implications.
Pull request overview
Implements Fleet Node outage awareness across the stack so miners assigned to an unreachable Fleet Node are treated as effectively OFFLINE while preserving the stored device status, enabling consistent filtering/counting behavior and clearer UI messaging (“Unavailable” / “No connection”), plus a least-privilege Grafana alert for node heartbeat staleness.
Changes:
- Derives an effective miner
OFFLINEstatus when the assigned Fleet Node is stale, and threads that effective status through list/filter/count/selector query paths. - Extends the Fleet Management API with
DeviceOfflineReasonand maps Fleet Node staleness toDEVICE_OFFLINE_REASON_FLEET_NODE_UNAVAILABLE. - Adds a Grafana alert rule for Fleet Node unavailability with column-level DB grants that avoid exposing node names/keys.
File summaries
| File | Description |
|---|---|
| server/sqlc/queries/device.sql | Updates sqlc query logic to use an effective status that treats stale Fleet Nodes as OFFLINE. |
| server/monitoring/grafana/provisioning/alerting/proto-fleet-rules.yaml | Adds the “Fleet Node Unavailable” alert rule using fleet_node heartbeat staleness. |
| server/migrations/000145_grant_fleet_node_alert_access.up.sql | Grants Grafana HA read-only role column-level access needed for the outage rule. |
| server/migrations/000145_grant_fleet_node_alert_access.down.sql | Revokes the column-level access granted in the up migration. |
| server/internal/infrastructure/timescaledb/provisioned_rules_test.go | Ensures the packaged default rule set includes “Fleet Node Unavailable”. |
| server/internal/infrastructure/timescaledb/fleet_node_rule_test.go | Validates alert rule semantics (staleness window, UID length) and grant column allowlist. |
| server/internal/infrastructure/db/migration_bridges_test.go | Bumps expected migration version to 145. |
| server/internal/handlers/alerts/handler.go | Treats Fleet Node outage template as device-less/safe for alert-only readers and maps it to proto “unspecified”. |
| server/internal/handlers/alerts/handler_history_test.go | Adds coverage for visibility of Fleet Node outage summary without miner:read. |
| server/internal/domain/stores/sqlstores/device.go | Threads FleetNodeUnavailable through scan/mapping and uses effective status in state count paths. |
| server/internal/domain/stores/sqlstores/device_query_fragments.go | Introduces fleetNodeUnavailableExpr and effectiveDeviceStatusExpr and adds Fleet Node joins into common miner query fragments. |
| server/internal/domain/stores/sqlstores/device_query_builder_test.go | Adds tests asserting Fleet Node outages are derived as OFFLINE and shared across list/count queries. |
| server/internal/domain/stores/sqlstores/device_integration_test.go | Adds DB integration test verifying outage derivation and recovery behavior. |
| server/internal/domain/stores/sqlstores/device_filters.go | Switches filter predicates to use effective_status.status (including numeric-range offline exclusion). |
| server/internal/domain/stores/sqlstores/device_filters_test.go | Updates/extends tests to assert effective-status SQL output. |
| server/internal/domain/fleetmanagement/service.go | Maps FleetNodeUnavailable to OFFLINE + DEVICE_OFFLINE_REASON_FLEET_NODE_UNAVAILABLE. |
| server/internal/domain/fleetmanagement/service_test.go | Adds assertions that the new offline reason is set when FleetNodeUnavailable is true. |
| server/internal/domain/alerts/service.go | Parses the new fleet-node-unavailable template label into a typed RuleTemplate. |
| server/internal/domain/alerts/render.go | Adds generic resolved copy for Fleet Node outage alerts. |
| server/internal/domain/alerts/render_test.go | Adds Slack rendering coverage for generic Fleet Node outage copy. |
| server/internal/domain/alerts/models.go | Defines RuleTemplateFleetNodeUnavailable. |
| server/generated/sqlc/device.sql.go | Generated sqlc updates reflecting effective-status logic and new FleetNodeUnavailable column. |
| server/generated/grpc/fleetmanagement/v1/fleetmanagement.pb.go | Generated protobuf updates adding DeviceOfflineReason and offline_reason field. |
| server/cmd/fleetnode/run.go | Validates heartbeat interval and caps it at the 30s default maximum. |
| server/cmd/fleetnode/run_test.go | Adds tests for heartbeat interval validation behavior. |
| proto/fleetmanagement/v1/fleetmanagement.proto | Adds DeviceOfflineReason and MinerStateSnapshot.offline_reason. |
| deployment-files/run-fleet.sh | Extends Grafana provisioning to wait for fleet_node and grant only required columns; adds a smoke-check query. |
| client/src/protoFleet/features/settings/components/Nodes/NodeStatusBadge.tsx | Updates “stale” threshold to 2 minutes to match server-side outage semantics. |
| client/src/protoFleet/features/settings/components/Nodes/NodeStatusBadge.test.tsx | Updates badge boundary tests to match the new 2-minute threshold. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/stories/statusMocks.ts | Updates Storybook mocks to include offlineReason. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/stories/mocks.ts | Updates Storybook mocks to include offlineReason. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/MinerStatus.tsx | Renders “Unavailable” when offlineReason indicates Fleet Node unavailability. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/MinerStatus.test.tsx | Adds coverage for “Unavailable” rendering based on offlineReason. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/MinerIssues.tsx | Renders “No connection” and suppresses other issue UI when offlineReason indicates Fleet Node unavailability. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/MinerIssues.test.tsx | Adds coverage for “No connection” and icon/skeleton suppression for Fleet Node outages. |
| client/src/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb.ts | Generated TS protobuf updates adding offlineReason enum/field. |
Review details
- Files reviewed: 33/36 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Keep persisted history, rack slots, CSV exports, and HA alerts aligned with the effective offline status. Treat only previously connected nodes as unavailable and tighten Grafana access to the five required columns.
|
🤖
Addressed in |
Reviewable diff: +396/-83 across 22 files (excludes generated, test, and story files).
Summary
Miners assigned to a previously connected but unreachable Fleet Node now show Unavailable with No connection instead of presenting stale healthy data. The same outage classification is used by fleet lists, filters, counts, rack slots, CSV exports, and persisted uptime history. Standalone and HA deployments receive a critical Fleet Node outage alert using five non-secret database columns.
How it works
OFFLINEstatus for devices assigned to that node without changing the stored miner status. List, count, filter, selector, rack-slot, CSV, and historical-snapshot paths apply the same rule.OFFLINEstatus plusDEVICE_OFFLINE_REASON_FLEET_NODE_UNAVAILABLE. The client maps that reason toUnavailableandNo connectionwhile retaining existing offline action and measurement behavior.fleet_nodethrough a five-columnSELECTgrant. After the node has been unavailable for another three minutes, Grafana fires a generic critical alert without exposing node names or key material. In HA, both local Grafanas evaluate the rule, while only the active Fleet API accepts webhook delivery.flowchart LR FN["Fleet Node heartbeat"] --> STATUS["Effective miner status"] STATUS --> API["OFFLINE plus reason enum"] API --> UI["Unavailable and No connection"] API --> VIEWS["Lists, racks, CSV, counts, and selectors"] STATUS --> HISTORY["Persisted uptime snapshot"] FN --> RULES["Standalone and HA Grafana rules"] RULES --> ALERT["Generic outage alert and recovery"]stateDiagram-v2 [*] --> NeverConnected NeverConnected --> Available: first heartbeat Available --> Unavailable: last heartbeat older than two minutes Unavailable --> Alerting: unavailable for three more minutes Unavailable --> Available: heartbeat resumes Alerting --> Available: heartbeat resumes and alert resolvesAreas of the code involved
proto/fleetmanagement/DeviceOfflineReasonandMinerStateSnapshot.offline_reasonserver/internal/domain/stores/sqlstores/andserver/sqlc/queries/server/internal/domain/fleetmanagement/client/src/protoFleet/features/Unavailable,No connection, and the matching two-minute Nodes-page thresholdserver/migrations/000145_*anddeployment-files/run-fleet.shorg_id,id,last_seen_at,enrollment_status, anddeleted_atserver/monitoring/grafana/andserver/internal/domain/alerts/server/internal/handlers/alerts/server/cmd/fleetnode/**/generated/**,*.pb.go,*.pb.tsKey technical decisions & trade-offs
DeviceStatus.OFFLINEand add a reason enum instead of creating a Fleet Node-specific status, preserving existing offline behavior while allowing precise copy.last_seen_at; confirmation alone does not turn a never-connected node into an outage.run-fleet.shfor roles created after migrations.Testing & validation
just genis stable after regenerating protobuf and sqlc output.just lintpasses: buf, ESLint, and golangci-lint report no issues.bash -n deployment-files/run-fleet.shandgit diff --checkpass.Post-Deploy Monitoring & Validation
protofleet-fleet-node-unavailable,permission denied for table fleet_node, query errors, and alert evaluation errors.