ulysses/routes
Sheikh Rahat Mahmud 9180847c0e
feat(diagnostics): add consolidated service health endpoint for degraded-state reporting (#964)
* Add consolidated service health endpoint for degraded-state reporting

ROADMAP (High Priority) asks for "Better degraded-state reporting for
ChromaDB, SearXNG, email, ntfy, and provider probes." Until now there was no
single readout of which subsystems are actually working: /api/health is only a
liveness ping and each subsystem's signal lives in a different module, so a
misconfigured self-host install gives no consolidated picture.

This adds an admin-only GET /api/diagnostics/services endpoint backed by a new
src/service_health.py aggregator. Each subsystem reports a uniform
{name, status, detail, meta} where status is ok | degraded | down | disabled,
and the response rolls up an overall verdict (worst non-disabled status).

Probes are deliberately non-intrusive and safe to poll:
- ChromaDB: reads the .healthy flags on the RAG and memory vector stores.
- SearXNG: GET /healthz (2xx), falling back to the instance root (<500). No
  search query is run.
- ntfy: GET the server's built-in /v1/health. No test notification is sent.
- email: short IMAP connect+logout per configured account (no credentials in
  meta).
- providers: probe each enabled ModelEndpoint's model list (no api_key in meta).

Probe functions take their inputs as parameters and isolate the network call to
injectable callables, so they unit-test without touching the network (same
pattern as the merged provider-endpoint tests). Network probes run concurrently
off the event loop via asyncio.to_thread with bounded per-probe timeouts.

memory_vector is now passed into setup_diagnostics_routes (new optional param,
backward-compatible) so ChromaDB's vector-memory store can be reported too.

Tests: tests/test_service_health.py — 29 tests covering every status mapping
per subsystem, the overall rollup, and that no secrets leak into meta.

Verification:
  python -m pytest tests/test_service_health.py -q          # 29 passed
  python -m py_compile src/service_health.py routes/diagnostics_routes.py app.py
  python -m pytest tests/test_endpoint_resolver.py tests/test_provider_endpoints.py -q

Backend + tests only; an Admin/Settings UI badge that renders this endpoint is
a natural follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(diagnostics): bound service-health wall-clock and redact secrets

Addresses review on #964.

Blocker 1 — genuinely bounded wall-clock:
- providers_health and email_health now fan out per-item probes across a
  bounded thread pool (_bounded_map) with a hard total budget (_FANOUT_BUDGET),
  instead of probing endpoints/accounts sequentially. Stragglers are reported
  as a controlled `timeout` and never block; the pool is shut down with
  wait=False so the response returns on time regardless of endpoint/account
  count.
- The IMAP connect path now honors the service-health budget: _imap_connect
  gained a pass-through `timeout` param and the probe calls it with
  _PROBE_TIMEOUT instead of the default 15s.
- collect_service_health runs the four network subsystems concurrently, each
  under a per-subsystem deadline (_SUBSYSTEM_DEADLINE), with an overall
  wait_for ceiling (_AGGREGATE_DEADLINE) as a backstop.

Blocker 2 — no secret/raw-error leakage in the response:
- _safe_url strips userinfo, query, and fragment from every URL surfaced in
  meta (searxng instance, ntfy base, provider name fallback), keeping only
  scheme/host/port/path.
- _classify_error maps every probe failure to a controlled category token
  (timeout, connection_refused, dns_error, tls_error, network_error,
  http_error, auth_or_protocol_error, …) — raw str(exception), which can embed
  credentialed URLs or server text, is never returned.

Tests (tests/test_service_health.py, +tests/test_diagnostics_service_route.py):
- URL userinfo/query redaction for searxng/ntfy/providers.
- secret-bearing exception strings map to categories and don't leak.
- multiple slow providers/accounts stay bounded (single + 25-endpoint cases).
- subsystems run concurrently; aggregate deadline yields a controlled result.
- route-level unauthenticated (401) / non-admin (403) / admin (200) coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(diagnostics): isolate route tests so they don't leak module globals

The new route tests replaced src.service_health.collect_service_health and
routes.diagnostics_routes.require_admin via direct assignment, which persisted
for the rest of the pytest session. In CI's full alphabetical run that fake
collector (returning services=[]) leaked into the later collect_service_health
tests and failed them. Switch to monkeypatch.setattr so both are restored after
each test. No production code change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
2026-06-09 16:00:24 +01:00
..
__init__.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
admin_wipe_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
api_token_routes.py Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish 2026-06-09 09:46:19 +09:00
assistant_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
auth_routes.py fix(auth): sync file-backed and in-memory owner caches on user rename (#3397) 2026-06-09 10:19:45 +02:00
backup_routes.py fix: backup import dropping a user's skill on cross-tenant title/id collision (#2057) 2026-06-09 08:04:22 +02:00
calendar_routes.py refactor(uploads): centralize upload byte-limits in upload_limits.py (#3364) (#3518) 2026-06-09 01:24:30 +02:00
chat_helpers.py fix(auth): per-user allowed-models checklist ignores cache, [None] doesn't block (#3355) 2026-06-08 22:52:39 +02:00
chat_routes.py refactor(tools): remove dead workspace-confinement plumbing (#3590) 2026-06-09 08:30:50 +02:00
chatgpt_subscription_routes.py feat: add ChatGPT Subscription provider (#2876) 2026-06-08 10:19:18 +02:00
cleanup_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
codex_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
compare_routes.py fix(endpoint): scope secondary endpoint lookups by owner 2026-06-08 11:51:55 +01:00
contacts_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
cookbook_helpers.py fix(cookbook): allow spaces and non-ASCII characters in model directory paths (#3473) 2026-06-09 07:58:38 +02:00
cookbook_routes.py Restore dropped regression fixes 2026-06-09 10:31:43 +09:00
copilot_routes.py feat: add ChatGPT Subscription provider (#2876) 2026-06-08 10:19:18 +02:00
device_flow.py feat: add ChatGPT Subscription provider (#2876) 2026-06-08 10:19:18 +02:00
diagnostics_routes.py feat(diagnostics): add consolidated service health endpoint for degraded-state reporting (#964) 2026-06-09 16:00:24 +01:00
document_helpers.py fix: _resolve_user_upload_path crashes on a non-dict resolve_upload result (#1715) 2026-06-03 13:34:33 +09:00
document_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
editor_draft_routes.py Ignore invalid editor draft payloads (#1533) 2026-06-03 14:07:03 +09:00
email_helpers.py feat(diagnostics): add consolidated service health endpoint for degraded-state reporting (#964) 2026-06-09 16:00:24 +01:00
email_pollers.py fix: quote IMAP mailbox arguments (#2170) 2026-06-05 16:00:20 +02:00
email_routes.py refactor(uploads): centralize upload byte-limits in upload_limits.py (#3364) (#3518) 2026-06-09 01:24:30 +02:00
embedding_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
emoji_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
font_routes.py Keep compact font family names together (#1263) 2026-06-03 14:24:30 +09:00
gallery_helpers.py fix: gallery records raw instead of display dimensions for EXIF-rotated photos (#1667) 2026-06-03 14:23:04 +09:00
gallery_routes.py refactor(uploads): centralize upload byte-limits in upload_limits.py (#3364) (#3518) 2026-06-09 01:24:30 +02:00
history_routes.py fix(sessions): copy message metadata when forking a session (#3409) 2026-06-08 20:49:15 +02:00
hwfit_routes.py Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish 2026-06-09 09:46:19 +09:00
mcp_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
memory_routes.py refactor(uploads): centralize upload byte-limits in upload_limits.py (#3364) (#3518) 2026-06-09 01:24:30 +02:00
model_routes.py feat(providers): add NVIDIA AI provider endpoint support (#3456) 2026-06-09 11:06:12 +02:00
note_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
personal_routes.py refactor(uploads): centralize upload byte-limits in upload_limits.py (#3364) (#3518) 2026-06-09 01:24:30 +02:00
prefs_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
preset_routes.py fix(presets): scope expand-prompt model resolution to owner (#3477) 2026-06-08 21:12:02 +02:00
research_routes.py fix(endpoint): scope secondary endpoint lookups by owner 2026-06-08 11:51:55 +01:00
search_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
session_routes.py fix(sessions): keep fresh chats during auto tidy (#1871) 2026-06-09 01:06:20 +01:00
shell_routes.py fix(cookbook): don't 500 the packages panel when an optional package crashes on import (#2618) 2026-06-07 18:14:43 +02:00
signature_routes.py Constrain signature uploads to PNG data (#2844) 2026-06-05 13:17:43 +02:00
skills_routes.py fix(presets): scope expand-prompt model resolution to owner (#3477) 2026-06-08 21:12:02 +02:00
stt_routes.py refactor(uploads): centralize upload byte-limits in upload_limits.py (#3364) (#3518) 2026-06-09 01:24:30 +02:00
task_routes.py Merge branch 'dev' 2026-06-09 09:36:01 +09:00
tts_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
upload_routes.py Scope vision model resolution by owner (#3009) 2026-06-07 12:39:02 +02:00
vault_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
webhook_routes.py feat: add ChatGPT Subscription provider (#2876) 2026-06-08 10:19:18 +02:00