navi/backend/services/navi_admin/fleet.py

133 lines
5.5 KiB
Python
Raw Permalink Normal View History

"""Fleet fan-out for navi-admin.
Add navi-admin service (extraction #7) (#7) * Add navi-admin service (extraction #7) Net-new fleet admin aggregator on :8427 — no port from recon (recon has no /api/admin route; Phase A §3). Three @require_auth routes: GET /api/admin/fleet fan-out to all 6 navi-* /api/admin/<svc>/info + recon /api/health, merged; never 5xx (failures land in errors[]) GET /api/admin/recon/info recon /api/health wrapped in the info shape GET /api/admin/navi-admin/info self-describe Fan-out forwards the caller's X-Authentik-Username so the @require_auth upstreams accept it; per-service admin endpoints stay localhost-only (this is the single edge-exposed admin surface). Service discovery: hardcoded list in fleet.py (Option B). No secrets, no DB. Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit, nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit (first Caddy change since #2). 12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[], auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets, auth-required). Full monorepo suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes 1. Symmetric degraded-entry handling in fleet.build_fleet — every probed service now appears in `services` with a uniform degraded dict on failure (matches recon's existing pattern), AND in errors[]. Operators see "everything I tried + which broke" consistently. 2. Catch ValueError specifically in _get_json — non-JSON 200 responses now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes (round 2) 1. Unified degraded shape: wrap_recon_health calls _degraded_entry on failure — no more runtime.status vs runtime.recon_status asymmetry. Every probed service has the same shape on failure (runtime.status == 'unreachable'). recon-specific runtime fields (recon_status/recon_uptime/pipeline) remain only on the success path. 2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in 7 service app.py files + fleet.recon_git_sha. One implementation, one place to fix when behavior changes. Adds shared/tests (testpaths now includes "shared"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:21:00 -06:00
navi-admin is a stateless aggregator: it fans out over localhost to each
navi-* service's ``/api/admin/<svc>/info`` endpoint, merging them into one
fleet response. Every per-service admin endpoint is ``@require_auth``, so the
fan-out forwards the caller's validated ``X-Authentik-Username`` header —
otherwise the upstreams would 401.
Add navi-admin service (extraction #7) (#7) * Add navi-admin service (extraction #7) Net-new fleet admin aggregator on :8427 — no port from recon (recon has no /api/admin route; Phase A §3). Three @require_auth routes: GET /api/admin/fleet fan-out to all 6 navi-* /api/admin/<svc>/info + recon /api/health, merged; never 5xx (failures land in errors[]) GET /api/admin/recon/info recon /api/health wrapped in the info shape GET /api/admin/navi-admin/info self-describe Fan-out forwards the caller's X-Authentik-Username so the @require_auth upstreams accept it; per-service admin endpoints stay localhost-only (this is the single edge-exposed admin surface). Service discovery: hardcoded list in fleet.py (Option B). No secrets, no DB. Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit, nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit (first Caddy change since #2). 12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[], auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets, auth-required). Full monorepo suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes 1. Symmetric degraded-entry handling in fleet.build_fleet — every probed service now appears in `services` with a uniform degraded dict on failure (matches recon's existing pattern), AND in errors[]. Operators see "everything I tried + which broke" consistently. 2. Catch ValueError specifically in _get_json — non-JSON 200 responses now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes (round 2) 1. Unified degraded shape: wrap_recon_health calls _degraded_entry on failure — no more runtime.status vs runtime.recon_status asymmetry. Every probed service has the same shape on failure (runtime.status == 'unreachable'). recon-specific runtime fields (recon_status/recon_uptime/pipeline) remain only on the success path. 2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in 7 service app.py files + fleet.recon_git_sha. One implementation, one place to fix when behavior changes. Adds shared/tests (testpaths now includes "shared"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:21:00 -06:00
Service discovery: a hardcoded module-level list (Option B). The set of navi-*
services changes only when we ship a new extraction the same moment we'd be
editing this file to add it so an env list would add a moving part with no
payoff. (One source of truth: the ports/names live here only.)
"""
import os
import time
from concurrent.futures import ThreadPoolExecutor
import requests
# (service-name, port) for every shipped navi-* service. The admin-info path is
# always /api/admin/<service-name>/info. Add a row when a new extraction ships.
SERVICES = [
('navi-traffic', 8421), # #1 TomTom traffic tile proxy
('navi-config', 8422), # #2 deployment profile API
('navi-contacts', 8423), # #3 contacts + address book
('navi-landclass', 8424), # #4 PAD-US land classification
('navi-places', 8425), # #5 OSM place detail + enrichment
('navi-geo', 8426), # #6 geocode + reverse + reverse bundle
('navi-offroute', 8428), # #8 off-network routing + MVUM
Add navi-admin service (extraction #7) (#7) * Add navi-admin service (extraction #7) Net-new fleet admin aggregator on :8427 — no port from recon (recon has no /api/admin route; Phase A §3). Three @require_auth routes: GET /api/admin/fleet fan-out to all 6 navi-* /api/admin/<svc>/info + recon /api/health, merged; never 5xx (failures land in errors[]) GET /api/admin/recon/info recon /api/health wrapped in the info shape GET /api/admin/navi-admin/info self-describe Fan-out forwards the caller's X-Authentik-Username so the @require_auth upstreams accept it; per-service admin endpoints stay localhost-only (this is the single edge-exposed admin surface). Service discovery: hardcoded list in fleet.py (Option B). No secrets, no DB. Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit, nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit (first Caddy change since #2). 12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[], auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets, auth-required). Full monorepo suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes 1. Symmetric degraded-entry handling in fleet.build_fleet — every probed service now appears in `services` with a uniform degraded dict on failure (matches recon's existing pattern), AND in errors[]. Operators see "everything I tried + which broke" consistently. 2. Catch ValueError specifically in _get_json — non-JSON 200 responses now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes (round 2) 1. Unified degraded shape: wrap_recon_health calls _degraded_entry on failure — no more runtime.status vs runtime.recon_status asymmetry. Every probed service has the same shape on failure (runtime.status == 'unreachable'). recon-specific runtime fields (recon_status/recon_uptime/pipeline) remain only on the success path. 2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in 7 service app.py files + fleet.recon_git_sha. One implementation, one place to fix when behavior changes. Adds shared/tests (testpaths now includes "shared"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:21:00 -06:00
]
DEFAULT_FANOUT_TIMEOUT_S = 3.0
def fanout_timeout():
try:
return float(os.environ.get('NAVI_ADMIN_FANOUT_TIMEOUT_S', DEFAULT_FANOUT_TIMEOUT_S))
except (ValueError, TypeError):
return DEFAULT_FANOUT_TIMEOUT_S
def service_info_url(name, port):
return f'http://127.0.0.1:{port}/api/admin/{name}/info'
def _get_json(url, auth_user, timeout):
"""GET url, forwarding the auth header. Returns (json_or_None, latency_ms,
error_or_None) where error is 'timeout' | 'HTTP <code>' | exception name."""
headers = {'X-Authentik-Username': auth_user} if auth_user else {}
start = time.monotonic()
try:
resp = requests.get(url, headers=headers, timeout=timeout)
latency_ms = round((time.monotonic() - start) * 1000, 1)
if resp.status_code != 200:
return None, latency_ms, f'HTTP {resp.status_code}'
return resp.json(), latency_ms, None
except requests.Timeout:
return None, round((time.monotonic() - start) * 1000, 1), 'timeout'
except ValueError:
# 200 with a non-JSON body (e.g. a misrouted upstream serving HTML).
# json.JSONDecodeError subclasses ValueError — report it plainly.
return None, round((time.monotonic() - start) * 1000, 1), 'invalid JSON'
except Exception as exc:
return None, round((time.monotonic() - start) * 1000, 1), type(exc).__name__
def probe(name, url, auth_user, timeout=None):
"""One GET → (summary, full_json, error). summary is the {name, status,
latency_ms[, error]} shape the per-service admin endpoints use for deps."""
full, latency_ms, error = _get_json(url, auth_user, timeout or fanout_timeout())
summary = {'name': name, 'status': 'ok' if error is None else 'error',
'latency_ms': latency_ms}
if error:
summary['error'] = error
return summary, full, error
def _degraded_entry(name, port, error):
"""Uniform 'service was probed but failed' entry — matches the
build_info_response shape so callers see the same keys whether the service is
healthy or down. Used for every failure path, so there is exactly one
degraded shape: runtime.status == 'unreachable'."""
Add navi-admin service (extraction #7) (#7) * Add navi-admin service (extraction #7) Net-new fleet admin aggregator on :8427 — no port from recon (recon has no /api/admin route; Phase A §3). Three @require_auth routes: GET /api/admin/fleet fan-out to all 6 navi-* /api/admin/<svc>/info + recon /api/health, merged; never 5xx (failures land in errors[]) GET /api/admin/recon/info recon /api/health wrapped in the info shape GET /api/admin/navi-admin/info self-describe Fan-out forwards the caller's X-Authentik-Username so the @require_auth upstreams accept it; per-service admin endpoints stay localhost-only (this is the single edge-exposed admin surface). Service discovery: hardcoded list in fleet.py (Option B). No secrets, no DB. Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit, nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit (first Caddy change since #2). 12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[], auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets, auth-required). Full monorepo suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes 1. Symmetric degraded-entry handling in fleet.build_fleet — every probed service now appears in `services` with a uniform degraded dict on failure (matches recon's existing pattern), AND in errors[]. Operators see "everything I tried + which broke" consistently. 2. Catch ValueError specifically in _get_json — non-JSON 200 responses now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes (round 2) 1. Unified degraded shape: wrap_recon_health calls _degraded_entry on failure — no more runtime.status vs runtime.recon_status asymmetry. Every probed service has the same shape on failure (runtime.status == 'unreachable'). recon-specific runtime fields (recon_status/recon_uptime/pipeline) remain only on the success path. 2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in 7 service app.py files + fleet.recon_git_sha. One implementation, one place to fix when behavior changes. Adds shared/tests (testpaths now includes "shared"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:21:00 -06:00
return {
'service': name, 'version': 'unknown', 'port': port,
'config': {}, 'env': [],
'dependencies': [{'name': f'{name}-info', 'status': 'error', 'error': error}],
'filesystem': [],
'runtime': {'status': 'unreachable'},
}
def build_fleet(auth_user):
"""Fan out to all navi-* services in parallel; merge. Never raises.
Add navi-admin service (extraction #7) (#7) * Add navi-admin service (extraction #7) Net-new fleet admin aggregator on :8427 — no port from recon (recon has no /api/admin route; Phase A §3). Three @require_auth routes: GET /api/admin/fleet fan-out to all 6 navi-* /api/admin/<svc>/info + recon /api/health, merged; never 5xx (failures land in errors[]) GET /api/admin/recon/info recon /api/health wrapped in the info shape GET /api/admin/navi-admin/info self-describe Fan-out forwards the caller's X-Authentik-Username so the @require_auth upstreams accept it; per-service admin endpoints stay localhost-only (this is the single edge-exposed admin surface). Service discovery: hardcoded list in fleet.py (Option B). No secrets, no DB. Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit, nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit (first Caddy change since #2). 12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[], auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets, auth-required). Full monorepo suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes 1. Symmetric degraded-entry handling in fleet.build_fleet — every probed service now appears in `services` with a uniform degraded dict on failure (matches recon's existing pattern), AND in errors[]. Operators see "everything I tried + which broke" consistently. 2. Catch ValueError specifically in _get_json — non-JSON 200 responses now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes (round 2) 1. Unified degraded shape: wrap_recon_health calls _degraded_entry on failure — no more runtime.status vs runtime.recon_status asymmetry. Every probed service has the same shape on failure (runtime.status == 'unreachable'). recon-specific runtime fields (recon_status/recon_uptime/pipeline) remain only on the success path. 2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in 7 service app.py files + fleet.recon_git_sha. One implementation, one place to fix when behavior changes. Adds shared/tests (testpaths now includes "shared"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:21:00 -06:00
Returns {services: {<name>: <info>}, fetched_at: ISO8601, errors: [{service, error}]}.
Invariant: EVERY probed service appears in `services` a full info dict when
healthy, the uniform `_degraded_entry` shape (runtime.status == 'unreachable')
when it fails and `errors` is a parallel listing of which ones failed and
why."""
Add navi-admin service (extraction #7) (#7) * Add navi-admin service (extraction #7) Net-new fleet admin aggregator on :8427 — no port from recon (recon has no /api/admin route; Phase A §3). Three @require_auth routes: GET /api/admin/fleet fan-out to all 6 navi-* /api/admin/<svc>/info + recon /api/health, merged; never 5xx (failures land in errors[]) GET /api/admin/recon/info recon /api/health wrapped in the info shape GET /api/admin/navi-admin/info self-describe Fan-out forwards the caller's X-Authentik-Username so the @require_auth upstreams accept it; per-service admin endpoints stay localhost-only (this is the single edge-exposed admin surface). Service discovery: hardcoded list in fleet.py (Option B). No secrets, no DB. Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit, nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit (first Caddy change since #2). 12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[], auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets, auth-required). Full monorepo suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes 1. Symmetric degraded-entry handling in fleet.build_fleet — every probed service now appears in `services` with a uniform degraded dict on failure (matches recon's existing pattern), AND in errors[]. Operators see "everything I tried + which broke" consistently. 2. Catch ValueError specifically in _get_json — non-JSON 200 responses now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes (round 2) 1. Unified degraded shape: wrap_recon_health calls _degraded_entry on failure — no more runtime.status vs runtime.recon_status asymmetry. Every probed service has the same shape on failure (runtime.status == 'unreachable'). recon-specific runtime fields (recon_status/recon_uptime/pipeline) remain only on the success path. 2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in 7 service app.py files + fleet.recon_git_sha. One implementation, one place to fix when behavior changes. Adds shared/tests (testpaths now includes "shared"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:21:00 -06:00
timeout = fanout_timeout()
targets = [(name, port, service_info_url(name, port)) for name, port in SERVICES]
def _fetch(name, port, url):
_, full, error = probe(name, url, auth_user, timeout)
return name, port, full, error
services = {}
errors = []
with ThreadPoolExecutor(max_workers=len(targets)) as ex:
Add navi-admin service (extraction #7) (#7) * Add navi-admin service (extraction #7) Net-new fleet admin aggregator on :8427 — no port from recon (recon has no /api/admin route; Phase A §3). Three @require_auth routes: GET /api/admin/fleet fan-out to all 6 navi-* /api/admin/<svc>/info + recon /api/health, merged; never 5xx (failures land in errors[]) GET /api/admin/recon/info recon /api/health wrapped in the info shape GET /api/admin/navi-admin/info self-describe Fan-out forwards the caller's X-Authentik-Username so the @require_auth upstreams accept it; per-service admin endpoints stay localhost-only (this is the single edge-exposed admin surface). Service discovery: hardcoded list in fleet.py (Option B). No secrets, no DB. Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit, nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit (first Caddy change since #2). 12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[], auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets, auth-required). Full monorepo suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes 1. Symmetric degraded-entry handling in fleet.build_fleet — every probed service now appears in `services` with a uniform degraded dict on failure (matches recon's existing pattern), AND in errors[]. Operators see "everything I tried + which broke" consistently. 2. Catch ValueError specifically in _get_json — non-JSON 200 responses now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes (round 2) 1. Unified degraded shape: wrap_recon_health calls _degraded_entry on failure — no more runtime.status vs runtime.recon_status asymmetry. Every probed service has the same shape on failure (runtime.status == 'unreachable'). recon-specific runtime fields (recon_status/recon_uptime/pipeline) remain only on the success path. 2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in 7 service app.py files + fleet.recon_git_sha. One implementation, one place to fix when behavior changes. Adds shared/tests (testpaths now includes "shared"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:21:00 -06:00
futures = [ex.submit(_fetch, name, port, url) for name, port, url in targets]
for fut in futures:
name, port, full, error = fut.result()
services[name] = full if full is not None else _degraded_entry(name, port, error)
if error:
errors.append({'service': name, 'error': error})
return {
'services': services,
'fetched_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'errors': errors,
}
def dependency_summaries(auth_user):
"""{name, status, latency_ms[, error]} for each navi-* admin endpoint — the
same probes the fleet runs, reused for navi-admin's own /info
`dependencies`. Sequential (it's a rare, auth-gated call)."""
Add navi-admin service (extraction #7) (#7) * Add navi-admin service (extraction #7) Net-new fleet admin aggregator on :8427 — no port from recon (recon has no /api/admin route; Phase A §3). Three @require_auth routes: GET /api/admin/fleet fan-out to all 6 navi-* /api/admin/<svc>/info + recon /api/health, merged; never 5xx (failures land in errors[]) GET /api/admin/recon/info recon /api/health wrapped in the info shape GET /api/admin/navi-admin/info self-describe Fan-out forwards the caller's X-Authentik-Username so the @require_auth upstreams accept it; per-service admin endpoints stay localhost-only (this is the single edge-exposed admin surface). Service discovery: hardcoded list in fleet.py (Option B). No secrets, no DB. Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit, nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit (first Caddy change since #2). 12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[], auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets, auth-required). Full monorepo suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes 1. Symmetric degraded-entry handling in fleet.build_fleet — every probed service now appears in `services` with a uniform degraded dict on failure (matches recon's existing pattern), AND in errors[]. Operators see "everything I tried + which broke" consistently. 2. Catch ValueError specifically in _get_json — non-JSON 200 responses now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PR #7 review fixes (round 2) 1. Unified degraded shape: wrap_recon_health calls _degraded_entry on failure — no more runtime.status vs runtime.recon_status asymmetry. Every probed service has the same shape on failure (runtime.status == 'unreachable'). recon-specific runtime fields (recon_status/recon_uptime/pipeline) remain only on the success path. 2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in 7 service app.py files + fleet.recon_git_sha. One implementation, one place to fix when behavior changes. Adds shared/tests (testpaths now includes "shared"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:21:00 -06:00
timeout = fanout_timeout()
summaries = []
for name, port in SERVICES:
s, _, _ = probe(name, service_info_url(name, port), auth_user, timeout)
summaries.append(s)
return summaries