diff --git a/haiku_rag_slim/haiku/rag/ingester/api/routes/providers.py b/haiku_rag_slim/haiku/rag/ingester/api/routes/providers.py new file mode 100644 index 00000000..b2b9dc73 --- /dev/null +++ b/haiku_rag_slim/haiku/rag/ingester/api/routes/providers.py @@ -0,0 +1,38 @@ +import asyncio + +import httpx +from fastapi import APIRouter, Depends + +from haiku.rag.ingester.api.schemas import ProviderEndpoint, ProvidersResponse +from haiku.rag.ingester.api.server import APIState, get_state + +router = APIRouter(tags=["providers"]) + +# Short timeout — operators care that the endpoint is reachable right now, +# not that it might respond if we wait. A docling-serve that takes longer +# than this to answer /health is effectively down for ingest purposes. +_PROBE_TIMEOUT_S = 2.0 + + +async def _probe(client: httpx.AsyncClient, base_url: str) -> ProviderEndpoint: + url = f"{base_url.rstrip('/')}/health" + try: + response = await client.get(url) + except httpx.HTTPError as exc: + return ProviderEndpoint(base_url=base_url, reachable=False, error=str(exc)) + return ProviderEndpoint( + base_url=base_url, + reachable=response.is_success, + status_code=response.status_code, + ) + + +@router.get("/providers", response_model=ProvidersResponse) +async def providers(state: APIState = Depends(get_state)) -> ProvidersResponse: + """Probe configured external providers (currently docling-serve) and + return their reachability. Dashboard polls this to surface a downstream + outage that the ingester itself can only see via worker job failures.""" + base_urls = state.config.providers.docling_serve.base_urls + async with httpx.AsyncClient(timeout=_PROBE_TIMEOUT_S) as client: + results = await asyncio.gather(*(_probe(client, u) for u in base_urls)) + return ProvidersResponse(docling_serve=list(results)) diff --git a/haiku_rag_slim/haiku/rag/ingester/api/schemas.py b/haiku_rag_slim/haiku/rag/ingester/api/schemas.py index cc421fd1..9833923d 100644 --- a/haiku_rag_slim/haiku/rag/ingester/api/schemas.py +++ b/haiku_rag_slim/haiku/rag/ingester/api/schemas.py @@ -20,6 +20,24 @@ class HealthResponse(BaseModel): worker_breaker_consecutive_failures: int = 0 +class ProviderEndpoint(BaseModel): + base_url: str + reachable: bool + # Status code from the probe (200 on success, the HTTP code on a non-2xx + # response, or None when the probe failed before getting a response). + status_code: int | None = None + # Error string when reachable is False and we have one (DNS failure, + # connection refused, timeout). None on success or unknown failure. + error: str | None = None + + +class ProvidersResponse(BaseModel): + """Reachability snapshot of configured external providers. Probed + on demand when /providers is hit; not cached.""" + + docling_serve: list[ProviderEndpoint] + + class SourceSummary(BaseModel): source_id: str type: Literal["fs", "http", "s3", "webdav"] diff --git a/haiku_rag_slim/haiku/rag/ingester/api/server.py b/haiku_rag_slim/haiku/rag/ingester/api/server.py index f29f981c..2ef34bf0 100644 --- a/haiku_rag_slim/haiku/rag/ingester/api/server.py +++ b/haiku_rag_slim/haiku/rag/ingester/api/server.py @@ -39,6 +39,7 @@ def build_app( dlq, health, jobs, + providers, sources, stats, ) @@ -61,5 +62,6 @@ def build_app( app.include_router(sources.router, dependencies=auth_dep) app.include_router(dlq.router, dependencies=auth_dep) app.include_router(stats.router, dependencies=auth_dep) + app.include_router(providers.router, dependencies=auth_dep) return app diff --git a/haiku_rag_slim/haiku/rag/ingester/api/static/index.html b/haiku_rag_slim/haiku/rag/ingester/api/static/index.html index c1ed1bf5..8b598bf2 100644 --- a/haiku_rag_slim/haiku/rag/ingester/api/static/index.html +++ b/haiku_rag_slim/haiku/rag/ingester/api/static/index.html @@ -86,6 +86,7 @@ tr:last-child td { border-bottom: none; } tr:hover td { background: rgba(255, 255, 255, 0.02); } td.id, td.worker { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); } + td.retried { color: var(--warn); font-weight: 600; cursor: help; } td.uri { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; max-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -95,6 +96,10 @@ color: var(--dead); font-size: 12px; max-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + td.ok { + color: var(--succeeded); font-size: 12px; max-width: 0; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } td.actions { width: 1%; white-space: nowrap; } td.actions button { background: var(--bg); color: var(--text); border: 1px solid var(--border); @@ -141,8 +146,6 @@ - -
| Kind | URL | Status | Detail | +
|---|