code-coordinator 0.5.46__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Per-machine facts for two of the fleet deploy-lane checks (#1806).
|
|
2
|
+
|
|
3
|
+
Root cause of #1806: `fleet_deploy_lanes`'s ``~/.coord-cli-venv`` lane and
|
|
4
|
+
the whole of `fleet_tui_binary` used to be gathered by ``os.stat``-ing the
|
|
5
|
+
**daemon host's** filesystem
|
|
6
|
+
(``coord.health.fleet_snapshot.FleetHealthRefresher._daemon_host_facts``).
|
|
7
|
+
That is correct for a fact that can only exist on the daemon process itself
|
|
8
|
+
(``coord-serve``'s own venv) — it is wrong for these two, whose subject is a
|
|
9
|
+
path on the **operator's** machine, which is very often a different box
|
|
10
|
+
from the one running ``coord-serve``. When the two differ, the daemon was
|
|
11
|
+
reporting on its own, usually-absent, copy of paths that live somewhere
|
|
12
|
+
else entirely — and reporting the *opposite* of the truth (#1806's live
|
|
13
|
+
example: WARN "stale" while the operator's real binary was current;
|
|
14
|
+
UNKNOWN "no data" while the operator's real CLI venv was on the release
|
|
15
|
+
everyone believed was live).
|
|
16
|
+
|
|
17
|
+
These two **machine-scope** checks report the same two raw facts (CLI-venv
|
|
18
|
+
version; tui-binary mtime vs. newest ``tui/`` source mtime) from wherever
|
|
19
|
+
they actually run — every agent's own ``/health`` poll, the transport #1630
|
|
20
|
+
already built for exactly this "measure locally, judge centrally" split.
|
|
21
|
+
`coord.health.checks.fleet_deploy_lanes` aggregates these across every
|
|
22
|
+
``ctx.fleet.machines`` entry instead of trusting a single host's local
|
|
23
|
+
``os.stat`` to answer a fleet-wide question.
|
|
24
|
+
|
|
25
|
+
Absence is the *common* case here — most machines in a fleet have neither
|
|
26
|
+
an operator's CLI venv nor a local ``tui/`` checkout+build — so both probes
|
|
27
|
+
report that plainly as ``OK`` ("not present on this machine") rather than
|
|
28
|
+
``UNKNOWN``/``WARN``: a box that was never meant to have either lane must
|
|
29
|
+
not read as faulty just because it doesn't.
|
|
30
|
+
|
|
31
|
+
A third check, ``webapp_bundle``, joins these two for the same reason and
|
|
32
|
+
the same shape (#1834 lane 5, the phone-webapp bundle `coord web --dist`
|
|
33
|
+
serves): the machine actually running `coord web` is very often not the
|
|
34
|
+
daemon host either. It is deliberately **not** graded against the released
|
|
35
|
+
wheel's version the way ``agent_venv``/``spawned_coord`` are —
|
|
36
|
+
``deploy/coord-web-dist-build.timer`` publishes continuously off
|
|
37
|
+
``origin/main``'s SHA (#1543), decoupled on purpose from the
|
|
38
|
+
``~/.coord-venv`` release cadence, so "is it on the released version?" is
|
|
39
|
+
not a well-formed question for it. What *is* well-formed, and what this
|
|
40
|
+
checks — mirroring ``tui_binary`` exactly — is whether the live bundle is
|
|
41
|
+
older than the ``coord/dashboard/webapp/`` source tree it claims to have
|
|
42
|
+
been built from.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import subprocess
|
|
48
|
+
|
|
49
|
+
from coord.dist_name import CANDIDATE_NAMES
|
|
50
|
+
from coord.health.checks.agent_install import pip_show
|
|
51
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
52
|
+
from coord.health.registry import check
|
|
53
|
+
from coord.health.units import expand, shorten_path
|
|
54
|
+
|
|
55
|
+
# Same documented default locations `coord.health.fleet_snapshot` used to
|
|
56
|
+
# stat on the daemon host — now stat'd wherever this check actually runs.
|
|
57
|
+
# `health.cli_venv_python` / `health.tui_binary_path` / `health.tui_source_dir`
|
|
58
|
+
# override these, same convention as `agent_venv_python`: None means "use the
|
|
59
|
+
# documented default location", NOT "disable the lane".
|
|
60
|
+
_DEFAULT_CLI_VENV_PYTHON = "~/.coord-cli-venv/bin/python3"
|
|
61
|
+
_DEFAULT_TUI_BINARY = "~/.local/bin/coord-tui"
|
|
62
|
+
# Mirrors `deploy/coord-web-dist-build.sh`'s `$LIVE_LINK` — the symlink
|
|
63
|
+
# `coord-web-dist-build.timer` atomically repoints at each new release.
|
|
64
|
+
_DEFAULT_WEBAPP_DIST = "~/coord-web-dist"
|
|
65
|
+
|
|
66
|
+
# Cap on the tui/ source walk, mirroring the old daemon-side walk: the tree
|
|
67
|
+
# is a few hundred .rs files, so anything past this is a misconfigured
|
|
68
|
+
# `tui_source_dir` pointed at something huge, and a machine's own /health
|
|
69
|
+
# tick must not spend its budget discovering that. A partial walk still
|
|
70
|
+
# yields a *lower bound* on the newest mtime — it can only under-report
|
|
71
|
+
# staleness, never fabricate it.
|
|
72
|
+
_MAX_TUI_SOURCE_FILES = 5000
|
|
73
|
+
# Same cap, same reasoning, for the webapp/ source walk below.
|
|
74
|
+
_MAX_WEBAPP_SOURCE_FILES = 5000
|
|
75
|
+
# Directories a webapp source walk must never descend into: dependency and
|
|
76
|
+
# build output trees that dwarf the actual source and would either blow the
|
|
77
|
+
# file cap on noise or (worse) let a stale `dist/` sitting inside a checkout
|
|
78
|
+
# masquerade as "source newer than itself".
|
|
79
|
+
_WEBAPP_SOURCE_SKIP_DIRS = {"node_modules", "dist", "build", "coverage"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def resolve_cli_venv_python(ctx: HealthContext):
|
|
83
|
+
"""The interpreter for this machine's ``~/.coord-cli-venv``, if any."""
|
|
84
|
+
configured = getattr(ctx.thresholds, "cli_venv_python", None)
|
|
85
|
+
if configured:
|
|
86
|
+
return expand(configured, ctx.home)
|
|
87
|
+
return expand(_DEFAULT_CLI_VENV_PYTHON, ctx.home)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def resolve_tui_binary_path(ctx: HealthContext):
|
|
91
|
+
"""This machine's locally-built ``coord-tui`` binary, if any."""
|
|
92
|
+
configured = getattr(ctx.thresholds, "tui_binary_path", None)
|
|
93
|
+
if configured:
|
|
94
|
+
return expand(configured, ctx.home)
|
|
95
|
+
return expand(_DEFAULT_TUI_BINARY, ctx.home)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def resolve_tui_source_dir(ctx: HealthContext):
|
|
99
|
+
"""``<checkout>/tui/src`` for the first local checkout that has one.
|
|
100
|
+
|
|
101
|
+
Derived from ``ctx.checkouts`` (the same locally-existing checkouts
|
|
102
|
+
every other checkout-scope probe sees) rather than guessed relative to
|
|
103
|
+
the binary path — a `target/release/...` install path says nothing
|
|
104
|
+
reliable about where the sources live, and comparing against the
|
|
105
|
+
*wrong* tree would manufacture staleness rather than report it.
|
|
106
|
+
Configured `health.tui_source_dir` wins outright when set.
|
|
107
|
+
"""
|
|
108
|
+
configured = getattr(ctx.thresholds, "tui_source_dir", None)
|
|
109
|
+
if configured:
|
|
110
|
+
return expand(configured, ctx.home)
|
|
111
|
+
for checkout in ctx.checkouts:
|
|
112
|
+
candidate = checkout.path / "tui" / "src"
|
|
113
|
+
if candidate.is_dir():
|
|
114
|
+
return candidate
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resolve_webapp_dist_path(ctx: HealthContext):
|
|
119
|
+
"""This machine's live ``coord web --dist`` bundle, if any (#1834)."""
|
|
120
|
+
configured = getattr(ctx.thresholds, "webapp_dist_path", None)
|
|
121
|
+
if configured:
|
|
122
|
+
return expand(configured, ctx.home)
|
|
123
|
+
return expand(_DEFAULT_WEBAPP_DIST, ctx.home)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def resolve_webapp_source_dir(ctx: HealthContext):
|
|
127
|
+
"""``<checkout>/coord/dashboard/webapp/src`` for the first local checkout
|
|
128
|
+
that has one. Same convention as :func:`resolve_tui_source_dir`."""
|
|
129
|
+
configured = getattr(ctx.thresholds, "webapp_source_dir", None)
|
|
130
|
+
if configured:
|
|
131
|
+
return expand(configured, ctx.home)
|
|
132
|
+
for checkout in ctx.checkouts:
|
|
133
|
+
candidate = checkout.path / "coord" / "dashboard" / "webapp" / "src"
|
|
134
|
+
if candidate.is_dir():
|
|
135
|
+
return candidate
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _newest_webapp_source_mtime(source_dir):
|
|
140
|
+
"""Newest mtime under *source_dir*, skipping dependency/build dirs.
|
|
141
|
+
|
|
142
|
+
Unlike :func:`_newest_rust_source_mtime` this is deliberately NOT
|
|
143
|
+
extension-filtered: a webapp source tree mixes ``.ts``/``.tsx``/``.css``/
|
|
144
|
+
``.html`` meaningfully and filtering would only risk under-reporting
|
|
145
|
+
staleness. ``node_modules``/``dist``/``build`` are the trees that
|
|
146
|
+
actually need skipping (size, and — for ``dist``/``build`` sitting
|
|
147
|
+
inside a checkout — self-reference), so those are excluded outright.
|
|
148
|
+
"""
|
|
149
|
+
newest = 0.0
|
|
150
|
+
seen = 0
|
|
151
|
+
try:
|
|
152
|
+
if not source_dir.is_dir():
|
|
153
|
+
return None
|
|
154
|
+
stack = [source_dir]
|
|
155
|
+
while stack:
|
|
156
|
+
current = stack.pop()
|
|
157
|
+
try:
|
|
158
|
+
entries = list(current.iterdir())
|
|
159
|
+
except OSError:
|
|
160
|
+
continue
|
|
161
|
+
for entry in entries:
|
|
162
|
+
name = entry.name
|
|
163
|
+
if name.startswith(".") or name in _WEBAPP_SOURCE_SKIP_DIRS:
|
|
164
|
+
continue
|
|
165
|
+
try:
|
|
166
|
+
if entry.is_dir():
|
|
167
|
+
stack.append(entry)
|
|
168
|
+
continue
|
|
169
|
+
newest = max(newest, entry.stat().st_mtime)
|
|
170
|
+
except OSError:
|
|
171
|
+
continue
|
|
172
|
+
seen += 1
|
|
173
|
+
if seen >= _MAX_WEBAPP_SOURCE_FILES:
|
|
174
|
+
stack.clear()
|
|
175
|
+
break
|
|
176
|
+
except OSError:
|
|
177
|
+
return newest or None
|
|
178
|
+
return newest or None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _newest_rust_source_mtime(source_dir):
|
|
182
|
+
"""Newest ``*.rs`` mtime under *source_dir*, or None if there are none.
|
|
183
|
+
|
|
184
|
+
Skips ``target``/hidden directories so a ``tui_source_dir`` pointed at a
|
|
185
|
+
crate root (rather than its ``src/``) degrades to slow-but-correct
|
|
186
|
+
instead of walking a multi-GB build tree.
|
|
187
|
+
"""
|
|
188
|
+
newest = 0.0
|
|
189
|
+
seen = 0
|
|
190
|
+
try:
|
|
191
|
+
if not source_dir.is_dir():
|
|
192
|
+
return None
|
|
193
|
+
stack = [source_dir]
|
|
194
|
+
while stack:
|
|
195
|
+
current = stack.pop()
|
|
196
|
+
try:
|
|
197
|
+
entries = list(current.iterdir())
|
|
198
|
+
except OSError:
|
|
199
|
+
continue
|
|
200
|
+
for entry in entries:
|
|
201
|
+
name = entry.name
|
|
202
|
+
if name.startswith(".") or name == "target":
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
if entry.is_dir():
|
|
206
|
+
stack.append(entry)
|
|
207
|
+
continue
|
|
208
|
+
if not name.endswith(".rs"):
|
|
209
|
+
continue
|
|
210
|
+
newest = max(newest, entry.stat().st_mtime)
|
|
211
|
+
except OSError:
|
|
212
|
+
continue
|
|
213
|
+
seen += 1
|
|
214
|
+
if seen >= _MAX_TUI_SOURCE_FILES:
|
|
215
|
+
stack.clear()
|
|
216
|
+
break
|
|
217
|
+
except OSError:
|
|
218
|
+
return newest or None
|
|
219
|
+
return newest or None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@check(
|
|
223
|
+
id="cli_venv",
|
|
224
|
+
scope="machine",
|
|
225
|
+
title="cli venv",
|
|
226
|
+
order=42,
|
|
227
|
+
description=(
|
|
228
|
+
"This machine's ~/.coord-cli-venv coordinator version, when "
|
|
229
|
+
"it has one — the operator's CLI venv lane of fleet_deploy_lanes (#1806)."
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
def probe_cli_venv(ctx: HealthContext) -> CheckResult:
|
|
233
|
+
python = resolve_cli_venv_python(ctx)
|
|
234
|
+
if not python.exists():
|
|
235
|
+
# The overwhelming common case: most machines are workers, not the
|
|
236
|
+
# operator's own box, and never had this venv created. That is not
|
|
237
|
+
# a fault — fleet_deploy_lanes reads "no machine reports this lane"
|
|
238
|
+
# as UNKNOWN, not this machine's absence as one.
|
|
239
|
+
return CheckResult(
|
|
240
|
+
check_id="cli_venv",
|
|
241
|
+
scope="machine",
|
|
242
|
+
severity=Severity.OK,
|
|
243
|
+
headroom="not present on this machine",
|
|
244
|
+
values={"python": str(python), "present": False, "version": None},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
fields = pip_show(python)
|
|
249
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
250
|
+
return CheckResult(
|
|
251
|
+
check_id="cli_venv",
|
|
252
|
+
scope="machine",
|
|
253
|
+
severity=Severity.UNKNOWN,
|
|
254
|
+
headroom=f"could not run pip show ({type(exc).__name__})",
|
|
255
|
+
error=str(exc),
|
|
256
|
+
values={"python": str(python), "present": True, "version": None},
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
version = fields.get("Version") or None
|
|
260
|
+
if not version:
|
|
261
|
+
tried = " or ".join(CANDIDATE_NAMES)
|
|
262
|
+
return CheckResult(
|
|
263
|
+
check_id="cli_venv",
|
|
264
|
+
scope="machine",
|
|
265
|
+
severity=Severity.UNKNOWN,
|
|
266
|
+
headroom=f"neither {tried} installed in {shorten_path(str(python), str(ctx.home))}",
|
|
267
|
+
error="pip show returned nothing",
|
|
268
|
+
values={"python": str(python), "present": True, "version": None},
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return CheckResult(
|
|
272
|
+
check_id="cli_venv",
|
|
273
|
+
scope="machine",
|
|
274
|
+
severity=Severity.OK,
|
|
275
|
+
headroom=f"pypi {version}",
|
|
276
|
+
values={
|
|
277
|
+
"python": str(python),
|
|
278
|
+
"present": True,
|
|
279
|
+
"version": version,
|
|
280
|
+
"editable": bool(fields.get("Editable project location")),
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@check(
|
|
286
|
+
id="tui_binary",
|
|
287
|
+
scope="machine",
|
|
288
|
+
title="tui binary",
|
|
289
|
+
order=43,
|
|
290
|
+
description=(
|
|
291
|
+
"This machine's locally-built tui/ binary vs. the tui/ source tree "
|
|
292
|
+
"it was built from — feeds the fleet_tui_binary check (#1806)."
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
def probe_tui_binary(ctx: HealthContext) -> CheckResult:
|
|
296
|
+
binary_path = resolve_tui_binary_path(ctx)
|
|
297
|
+
values: dict = {"path": str(binary_path)}
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
binary_mtime = binary_path.stat().st_mtime if binary_path.exists() else None
|
|
301
|
+
except OSError:
|
|
302
|
+
binary_mtime = None
|
|
303
|
+
|
|
304
|
+
if binary_mtime is None:
|
|
305
|
+
# Most machines never built a local tui/ binary — absent, not stale.
|
|
306
|
+
return CheckResult(
|
|
307
|
+
check_id="tui_binary",
|
|
308
|
+
scope="machine",
|
|
309
|
+
severity=Severity.OK,
|
|
310
|
+
headroom="not present on this machine",
|
|
311
|
+
values={**values, "present": False},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
values["present"] = True
|
|
315
|
+
values["binary_mtime"] = binary_mtime
|
|
316
|
+
|
|
317
|
+
source_dir = resolve_tui_source_dir(ctx)
|
|
318
|
+
if source_dir is None:
|
|
319
|
+
return CheckResult(
|
|
320
|
+
check_id="tui_binary",
|
|
321
|
+
scope="machine",
|
|
322
|
+
severity=Severity.OK,
|
|
323
|
+
headroom="binary present (tui/ source tree not found to compare)",
|
|
324
|
+
values=values,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
values["source_dir"] = str(source_dir)
|
|
328
|
+
source_mtime = _newest_rust_source_mtime(source_dir)
|
|
329
|
+
if source_mtime is None:
|
|
330
|
+
return CheckResult(
|
|
331
|
+
check_id="tui_binary",
|
|
332
|
+
scope="machine",
|
|
333
|
+
severity=Severity.OK,
|
|
334
|
+
headroom="binary present (tui/ source tree not found to compare)",
|
|
335
|
+
values=values,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
values["source_mtime"] = source_mtime
|
|
339
|
+
if source_mtime > binary_mtime:
|
|
340
|
+
stale_hours = (source_mtime - binary_mtime) / 3600.0
|
|
341
|
+
return CheckResult(
|
|
342
|
+
check_id="tui_binary",
|
|
343
|
+
scope="machine",
|
|
344
|
+
severity=Severity.WARN,
|
|
345
|
+
headroom=f"binary is {stale_hours:.1f}h older than tui/ source",
|
|
346
|
+
detail="rebuild the tui/ binary — source changed since the last local build",
|
|
347
|
+
threshold="warn when source/ is newer than the built binary",
|
|
348
|
+
values=values,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return CheckResult(
|
|
352
|
+
check_id="tui_binary",
|
|
353
|
+
scope="machine",
|
|
354
|
+
severity=Severity.OK,
|
|
355
|
+
headroom="up to date with tui/ source",
|
|
356
|
+
values=values,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@check(
|
|
361
|
+
id="webapp_bundle",
|
|
362
|
+
scope="machine",
|
|
363
|
+
title="webapp bundle",
|
|
364
|
+
order=47,
|
|
365
|
+
description=(
|
|
366
|
+
"This machine's live `coord web --dist` bundle vs. the "
|
|
367
|
+
"coord/dashboard/webapp/ source tree it was supposedly built from — "
|
|
368
|
+
"feeds fleet_webapp_bundle (#1834 lane 5)."
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
def probe_webapp_bundle(ctx: HealthContext) -> CheckResult:
|
|
372
|
+
"""The webapp analogue of :func:`probe_tui_binary`: same shape, same
|
|
373
|
+
fail-soft-to-absent convention, different subject.
|
|
374
|
+
|
|
375
|
+
Not graded against ``OWN_VERSION`` or the released wheel — see this
|
|
376
|
+
module's docstring — so this only ever reports staleness relative to the
|
|
377
|
+
``webapp/`` source tree, never a version.
|
|
378
|
+
"""
|
|
379
|
+
dist_path = resolve_webapp_dist_path(ctx)
|
|
380
|
+
values: dict = {"path": str(dist_path)}
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
present = dist_path.exists()
|
|
384
|
+
except OSError:
|
|
385
|
+
present = False
|
|
386
|
+
|
|
387
|
+
if not present:
|
|
388
|
+
# Most machines never run `coord web --dist` — absent, not stale.
|
|
389
|
+
return CheckResult(
|
|
390
|
+
check_id="webapp_bundle",
|
|
391
|
+
scope="machine",
|
|
392
|
+
severity=Severity.OK,
|
|
393
|
+
headroom="not present on this machine",
|
|
394
|
+
values={**values, "present": False},
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
values["present"] = True
|
|
398
|
+
try:
|
|
399
|
+
resolved = dist_path.resolve()
|
|
400
|
+
# `coord-web-dist-build.sh` names each release directory after the
|
|
401
|
+
# SHA it built — the symlink target's basename doubles as "which
|
|
402
|
+
# commit this machine is actually serving" whenever that convention
|
|
403
|
+
# holds, and is simply absent (not fabricated) when it doesn't.
|
|
404
|
+
values["sha"] = resolved.name
|
|
405
|
+
except OSError:
|
|
406
|
+
resolved = dist_path
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
dist_mtime = resolved.stat().st_mtime
|
|
410
|
+
except OSError:
|
|
411
|
+
dist_mtime = None
|
|
412
|
+
values["dist_mtime"] = dist_mtime
|
|
413
|
+
|
|
414
|
+
source_dir = resolve_webapp_source_dir(ctx)
|
|
415
|
+
if source_dir is None or dist_mtime is None:
|
|
416
|
+
return CheckResult(
|
|
417
|
+
check_id="webapp_bundle",
|
|
418
|
+
scope="machine",
|
|
419
|
+
severity=Severity.OK,
|
|
420
|
+
headroom="bundle present (webapp/ source tree not found to compare)",
|
|
421
|
+
values=values,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
values["source_dir"] = str(source_dir)
|
|
425
|
+
source_mtime = _newest_webapp_source_mtime(source_dir)
|
|
426
|
+
if source_mtime is None:
|
|
427
|
+
return CheckResult(
|
|
428
|
+
check_id="webapp_bundle",
|
|
429
|
+
scope="machine",
|
|
430
|
+
severity=Severity.OK,
|
|
431
|
+
headroom="bundle present (webapp/ source tree not found to compare)",
|
|
432
|
+
values=values,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
values["source_mtime"] = source_mtime
|
|
436
|
+
if source_mtime > dist_mtime:
|
|
437
|
+
stale_hours = (source_mtime - dist_mtime) / 3600.0
|
|
438
|
+
return CheckResult(
|
|
439
|
+
check_id="webapp_bundle",
|
|
440
|
+
scope="machine",
|
|
441
|
+
severity=Severity.WARN,
|
|
442
|
+
headroom=f"bundle is {stale_hours:.1f}h older than webapp/ source",
|
|
443
|
+
detail=(
|
|
444
|
+
"coord-web-dist-build.timer has not published since this "
|
|
445
|
+
"source changed — check `systemctl --user status "
|
|
446
|
+
"coord-web-dist-build.timer` on this machine"
|
|
447
|
+
),
|
|
448
|
+
threshold="warn when webapp/ source is newer than the live bundle",
|
|
449
|
+
values=values,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return CheckResult(
|
|
453
|
+
check_id="webapp_bundle",
|
|
454
|
+
scope="machine",
|
|
455
|
+
severity=Severity.OK,
|
|
456
|
+
headroom="up to date with webapp/ source",
|
|
457
|
+
values=values,
|
|
458
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Disk headroom per configured mount point (#1628).
|
|
2
|
+
|
|
3
|
+
The 2026-07-30 incident that motivated this milestone: elitebook's ``/home``
|
|
4
|
+
reached **0 bytes free** and every worker on that machine failed in a way
|
|
5
|
+
that looked like a coordinator bug. Nothing was watching the one number
|
|
6
|
+
that would have said so a day earlier.
|
|
7
|
+
|
|
8
|
+
Thresholds are expressed as percent *free* — headroom — rather than percent
|
|
9
|
+
used, because "how much is left" is the question, and because the rendered
|
|
10
|
+
line still shows used% for the operator who thinks in those terms.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
|
|
18
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
19
|
+
from coord.health.registry import check
|
|
20
|
+
from coord.health.units import expand, human_bytes
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@check(
|
|
24
|
+
id="disk",
|
|
25
|
+
scope="machine",
|
|
26
|
+
title="disk",
|
|
27
|
+
order=10,
|
|
28
|
+
description="Free space on the filesystems the fleet writes to.",
|
|
29
|
+
)
|
|
30
|
+
def probe_disk(ctx: HealthContext) -> list[CheckResult]:
|
|
31
|
+
"""One result per distinct filesystem behind ``thresholds.disk_paths``.
|
|
32
|
+
|
|
33
|
+
Paths that don't exist are skipped silently (``/home`` is not a separate
|
|
34
|
+
mount everywhere). Paths that resolve to a filesystem already reported
|
|
35
|
+
are skipped too — on a single-root machine ``/``, ``/home`` and
|
|
36
|
+
``~/.coord`` are the same device, and three identical CRIT lines is noise
|
|
37
|
+
that trains an operator to skim.
|
|
38
|
+
"""
|
|
39
|
+
th = ctx.thresholds
|
|
40
|
+
results: list[CheckResult] = []
|
|
41
|
+
seen_devices: set[int] = set()
|
|
42
|
+
|
|
43
|
+
for raw in th.disk_paths:
|
|
44
|
+
path = expand(raw, ctx.home)
|
|
45
|
+
try:
|
|
46
|
+
device = os.stat(path).st_dev
|
|
47
|
+
except OSError:
|
|
48
|
+
continue # not present on this machine — not a finding
|
|
49
|
+
if device in seen_devices:
|
|
50
|
+
continue
|
|
51
|
+
seen_devices.add(device)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
usage = shutil.disk_usage(str(path))
|
|
55
|
+
except OSError as exc:
|
|
56
|
+
results.append(
|
|
57
|
+
CheckResult(
|
|
58
|
+
check_id="disk",
|
|
59
|
+
scope="machine",
|
|
60
|
+
subject=str(raw),
|
|
61
|
+
severity=Severity.UNKNOWN,
|
|
62
|
+
headroom=f"could not stat filesystem: {exc}",
|
|
63
|
+
error=str(exc),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
total = float(usage.total)
|
|
69
|
+
free_pct = (usage.free / total * 100.0) if total > 0 else 0.0
|
|
70
|
+
used_pct = 100.0 - free_pct
|
|
71
|
+
|
|
72
|
+
if free_pct < th.disk_crit_free_pct:
|
|
73
|
+
severity = Severity.CRIT
|
|
74
|
+
elif free_pct < th.disk_warn_free_pct:
|
|
75
|
+
severity = Severity.WARN
|
|
76
|
+
else:
|
|
77
|
+
severity = Severity.OK
|
|
78
|
+
|
|
79
|
+
results.append(
|
|
80
|
+
CheckResult(
|
|
81
|
+
check_id="disk",
|
|
82
|
+
scope="machine",
|
|
83
|
+
subject=str(raw),
|
|
84
|
+
severity=severity,
|
|
85
|
+
headroom=f"{used_pct:.0f}% used ({human_bytes(usage.free)} free)",
|
|
86
|
+
threshold=f"crit at {100.0 - th.disk_crit_free_pct:.0f}%",
|
|
87
|
+
values={
|
|
88
|
+
"path": str(path),
|
|
89
|
+
"total_bytes": usage.total,
|
|
90
|
+
"free_bytes": usage.free,
|
|
91
|
+
"used_bytes": usage.used,
|
|
92
|
+
"free_pct": round(free_pct, 2),
|
|
93
|
+
"used_pct": round(used_pct, 2),
|
|
94
|
+
"warn_free_pct": th.disk_warn_free_pct,
|
|
95
|
+
"crit_free_pct": th.disk_crit_free_pct,
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
return results
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Fleet-scope: the board daemon's own ``/board`` latency + payload size (#1630, #1597).
|
|
2
|
+
|
|
3
|
+
A 5.3MB ``/board`` payload already broke ``coord report-result`` once (#1336)
|
|
4
|
+
by blowing a 5s prefetch timeout — this check exists so that regression shows
|
|
5
|
+
up as a fleet-health row instead of a mysterious timeout report weeks later.
|
|
6
|
+
Like the other fleet checks in this package, the daemon's ``/board`` handler
|
|
7
|
+
(``coord.serve_app``) is the thing that actually measures its own latency and
|
|
8
|
+
serialized body size on each rebuild; this probe only judges the numbers it's
|
|
9
|
+
handed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
15
|
+
from coord.health.registry import check
|
|
16
|
+
from coord.health.units import human_bytes
|
|
17
|
+
|
|
18
|
+
# #1336: the 5.3MB payload that broke a 5s prefetch. Warn well before that.
|
|
19
|
+
_PAYLOAD_WARN_BYTES = 2 * 1024 * 1024
|
|
20
|
+
_PAYLOAD_CRIT_BYTES = 5 * 1024 * 1024
|
|
21
|
+
_LATENCY_WARN_MS = 1500.0
|
|
22
|
+
_LATENCY_CRIT_MS = 4000.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@check(
|
|
26
|
+
id="fleet_board_latency",
|
|
27
|
+
scope="fleet",
|
|
28
|
+
title="board latency+size",
|
|
29
|
+
order=20,
|
|
30
|
+
description="The daemon's own /board rebuild latency and serialized payload size.",
|
|
31
|
+
)
|
|
32
|
+
def probe_board_latency(ctx: HealthContext) -> CheckResult:
|
|
33
|
+
if ctx.fleet is None:
|
|
34
|
+
return CheckResult(
|
|
35
|
+
check_id="fleet_board_latency",
|
|
36
|
+
scope="fleet",
|
|
37
|
+
severity=Severity.UNKNOWN,
|
|
38
|
+
headroom="no fleet snapshot (fleet checks only run on the daemon)",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
dh = ctx.fleet.daemon_host or {}
|
|
42
|
+
latency_ms = dh.get("board_latency_ms")
|
|
43
|
+
payload_bytes = dh.get("board_payload_bytes")
|
|
44
|
+
|
|
45
|
+
if latency_ms is None and payload_bytes is None:
|
|
46
|
+
return CheckResult(
|
|
47
|
+
check_id="fleet_board_latency",
|
|
48
|
+
scope="fleet",
|
|
49
|
+
severity=Severity.UNKNOWN,
|
|
50
|
+
headroom="no /board build measured yet",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
severity = Severity.OK
|
|
54
|
+
reasons: list[str] = []
|
|
55
|
+
if payload_bytes is not None:
|
|
56
|
+
if payload_bytes >= _PAYLOAD_CRIT_BYTES:
|
|
57
|
+
severity = Severity.CRIT
|
|
58
|
+
reasons.append(f"payload {human_bytes(payload_bytes)}")
|
|
59
|
+
elif payload_bytes >= _PAYLOAD_WARN_BYTES:
|
|
60
|
+
severity = Severity.WARN
|
|
61
|
+
reasons.append(f"payload {human_bytes(payload_bytes)}")
|
|
62
|
+
if latency_ms is not None:
|
|
63
|
+
if latency_ms >= _LATENCY_CRIT_MS:
|
|
64
|
+
severity = Severity.CRIT
|
|
65
|
+
reasons.append(f"latency {latency_ms:.0f}ms")
|
|
66
|
+
elif latency_ms >= _LATENCY_WARN_MS and severity is not Severity.CRIT:
|
|
67
|
+
severity = Severity.WARN
|
|
68
|
+
reasons.append(f"latency {latency_ms:.0f}ms")
|
|
69
|
+
|
|
70
|
+
parts = []
|
|
71
|
+
if latency_ms is not None:
|
|
72
|
+
parts.append(f"{latency_ms:.0f}ms")
|
|
73
|
+
if payload_bytes is not None:
|
|
74
|
+
parts.append(human_bytes(payload_bytes))
|
|
75
|
+
headroom = " / ".join(parts) if parts else "no data"
|
|
76
|
+
if severity is not Severity.OK:
|
|
77
|
+
headroom += f" ({', '.join(reasons)})"
|
|
78
|
+
|
|
79
|
+
return CheckResult(
|
|
80
|
+
check_id="fleet_board_latency",
|
|
81
|
+
scope="fleet",
|
|
82
|
+
severity=severity,
|
|
83
|
+
headroom=headroom,
|
|
84
|
+
threshold=(
|
|
85
|
+
f"warn at {human_bytes(_PAYLOAD_WARN_BYTES)}/{_LATENCY_WARN_MS:.0f}ms, "
|
|
86
|
+
f"crit at {human_bytes(_PAYLOAD_CRIT_BYTES)}/{_LATENCY_CRIT_MS:.0f}ms"
|
|
87
|
+
),
|
|
88
|
+
values={"latency_ms": latency_ms, "payload_bytes": payload_bytes},
|
|
89
|
+
)
|