Rebuild the FAISS + BM25 hybrid index from knowledge files in the KB directory.
Loading RAG status…
📋 Request Metrics
Loading…
📈 Cloudflare Analytics (Persistent)
Loading…
📋 Recent Queries (Cloudflare D1)
Loading…
📊 Reports
Choose a report and click "Show report".
🔌 System Status
Loading…
""")
# ── API Endpoints ──────────────────────────────────────────────────────────────
@router.get("/public-config")
async def public_config():
"""No auth — returns public fields for the chat frontend."""
try:
cfg = _load_config()
except Exception:
cfg = get_settings().runtime_defaults
return {
"welcome_message": str(cfg.get("welcome_message", ""))[:500],
"announcement": str(cfg.get("announcement", ""))[:500],
"maintenance_mode": bool(cfg.get("maintenance_mode", False)),
"maintenance_message": str(cfg.get("maintenance_message", ""))[:300],
}
@router.get("/worker-browser-token")
async def worker_browser_token(request: Request):
"""Issue a short-lived read-only Worker token to a logged-in administrator."""
_require_session(request)
settings = get_settings()
worker_url = _clean_worker_url(settings.cloudflare_worker_url)
if not worker_url:
raise HTTPException(status_code=503, detail="CLOUDFLARE_WORKER_URL is missing or invalid")
if not settings.cloudflare_admin_token:
raise HTTPException(status_code=503, detail="CLOUDFLARE_ADMIN_TOKEN is not configured")
token, expires_at = _make_worker_browser_token(settings.cloudflare_admin_token)
return {
"worker_url": worker_url,
"token": token,
"expires_at": expires_at,
"scope": "analytics:read",
}
@router.get("/config")
async def get_config(request: Request):
_require_session(request)
return _load_config()
@router.post("/config")
async def update_config(req: ConfigUpdate, request: Request):
_require_session(request)
cleaned = _validated_config_values({req.key: req.value})
data = _config_store().load()
data.update(cleaned)
_config_store().save(data)
return {"status": "ok", "updated": req.key, "value": cleaned[req.key]}
@router.post("/config-bulk")
async def update_config_bulk(req: ConfigBulkUpdate, request: Request):
_require_session(request)
# Validate everything before writing; invalid values used to be skipped
# silently even while the dashboard displayed "Saved".
cleaned = _validated_config_values(req.settings)
data = _config_store().load()
data.update(cleaned)
_config_store().save(data)
return {"status": "ok", "updated": sorted(cleaned)}
# ── RAG Management Endpoints ───────────────────────────────────────────────────
@router.get("/rag-status")
async def rag_status(request: Request):
_require_session(request)
try:
from app import get_rag_service
return get_rag_service().get_stats()
except Exception as e:
return {"error": str(e), "ready": False}
@router.post("/rebuild")
async def rebuild_rag(request: Request):
_require_session(request)
try:
from app import get_rag_service
svc = get_rag_service()
settings = get_settings()
await svc.initialize(openai_api_key=settings.openai_api_key, force_rebuild=True)
if not svc.is_ready():
return {"status": "error", "error": "No usable knowledge index. Check knowledge files and server logs.", "ready": False}
return {"status": "ok", "chunks": len(svc.bm25_corpus), "ready": True,
"retrieval_mode": svc.get_stats().get("retrieval_mode", "unknown")}
except Exception as e:
logger.error("Admin index rebuild failed: %s", type(e).__name__)
return {"status": "error", "error": "Index rebuild failed. Check server logs."}
# ── Metrics & Status Endpoints ─────────────────────────────────────────────────
@router.get("/metrics")
async def metrics(request: Request):
_require_session(request)
try:
from app import get_metrics_service
return get_metrics_service().snapshot()
except Exception as e:
return {"error": str(e)}
@router.get("/status")
async def system_status(request: Request):
_require_session(request)
settings = get_settings()
mem_used_mb = mem_pct = 0
try:
with open("/sys/fs/cgroup/memory.current") as f:
used = int(f.read().strip())
with open("/sys/fs/cgroup/memory.max") as f:
val = f.read().strip()
total = int(val) if val != "max" else 16 * 1024 ** 3
mem_used_mb = round(used / 1024 / 1024)
mem_pct = round(used / total * 100)
except Exception:
try:
with open("/sys/fs/cgroup/memory/memory.usage_in_bytes") as f:
used = int(f.read().strip())
with open("/sys/fs/cgroup/memory/memory.limit_in_bytes") as f:
total = int(f.read().strip())
mem_used_mb = round(used / 1024 / 1024)
mem_pct = round(used / total * 100)
except Exception:
pass
rag_ready = False
rag_chunks = 0
kb_files = 0
try:
from app import get_rag_service
svc = get_rag_service()
rag_ready = svc.is_ready()
rag_chunks = len(svc.bm25_corpus)
kb_dir = settings.kb_dir
kb_files = len(list(kb_dir.glob("*.txt"))) if kb_dir.exists() else 0
except Exception:
pass
cfg = _load_config()
return {
"status": "ok",
"rag_ready": rag_ready,
"rag_chunks": rag_chunks,
"kb_files": kb_files,
"openai": bool(settings.openai_api_key),
"anthropic": bool(settings.anthropic_api_key),
"primo": bool(settings.primo_api_key),
"mem_used_mb": mem_used_mb,
"mem_pct": mem_pct,
"uptime_secs": round(time.time() - _START_TIME),
"maintenance": bool(cfg.get("maintenance_mode")),
"version": LIBBEE_VERSION,
}
# ── Daily KB-gap / relevance reports (generated by the Cloudflare Worker) ─────
# The Worker's daily cron writes one report row per day; these endpoints proxy
# them into the admin dashboard. Session-protected like every admin route, and
# the Worker itself additionally requires the shared bearer token.
@router.get("/reports")
async def admin_reports(request: Request, days: int = 14):
_require_session(request)
return await _worker_get("/reports", {"days": max(1, min(days, 60))})
@router.post("/reports/{report_id}/reviewed")
async def admin_report_reviewed(report_id: int, request: Request):
_require_session(request)
import httpx as _hx
from src.config import get_settings as _gs
settings = _gs()
worker = _clean_worker_url(settings.cloudflare_worker_url)
if not worker:
raise HTTPException(status_code=503, detail="CLOUDFLARE_WORKER_URL not configured")
headers = {}
if settings.cloudflare_admin_token:
headers["Authorization"] = f"Bearer {settings.cloudflare_admin_token}"
try:
async with _hx.AsyncClient(timeout=10) as client:
r = await client.post(f"{worker}/reports/{int(report_id)}/reviewed", headers=headers)
if r.status_code >= 400:
raise HTTPException(status_code=502, detail=f"Worker returned HTTP {r.status_code}")
return r.json()
except Exception as exc:
raise HTTPException(status_code=502, detail="Worker unavailable. Check the configured URL and server logs.")
# ── Analytics proxies ────────────────────────────────────────────────────────
# The dashboard fetches Worker analytics through these session-protected
# proxies so the Worker URL and ADMIN_TOKEN never reach the browser.
async def _worker_get(path: str, params: Optional[dict] = None):
from src.services.http_client import make_client as _mk
from src.config import get_settings as _gs
settings = _gs()
worker = _clean_worker_url(settings.cloudflare_worker_url)
if not worker:
raise HTTPException(status_code=503, detail="CLOUDFLARE_WORKER_URL not configured")
headers = {}
if settings.cloudflare_admin_token:
headers["Authorization"] = f"Bearer {settings.cloudflare_admin_token}"
try:
async with _mk(timeout=8) as client:
r = await client.get(f"{worker}{path}", params=params or {}, headers=headers)
if r.status_code >= 400:
raise HTTPException(status_code=502, detail=f"Worker returned HTTP {r.status_code}")
return _escape_untrusted_json(r.json())
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=502, detail="Worker unavailable. Check the configured URL and server logs.")
@router.get("/analytics")
async def admin_analytics(request: Request):
_require_session(request)
return await _worker_get("/analytics")
@router.get("/analytics/recent")
async def admin_analytics_recent(request: Request):
_require_session(request)
return await _worker_get("/analytics/recent")
@router.get("/worker-health")
async def admin_worker_health(request: Request):
"""Diagnose the backend -> Worker -> D1 chain.
Every probe runs independently and records its own outcome, including the
exception type, so one failure never hides the rest. Also reports the exact
URL being used (whitespace or quotes in the secret are a common cause).
"""
_require_session(request)
import asyncio as _aio
from src.services.http_client import make_client as _mk
from src.config import get_settings as _gs, LIBBEE_VERSION as _ver
settings = _gs()
raw = settings.cloudflare_worker_url or ""
worker = _clean_worker_url(raw)
token = (settings.cloudflare_admin_token or "").strip()
out = {
"backend_version": _ver,
"worker_url_configured": bool(worker),
"worker_url_used": worker,
"worker_url_had_whitespace_or_quotes": raw != worker + ("/" if raw.endswith("/") else ""),
"worker_token_configured": bool(token),
"worker_token_length": len(token),
"probes": {},
"verdict": "",
}
if not worker:
out["verdict"] = "CLOUDFLARE_WORKER_URL is not set in the Space secrets."
return out
headers = {"Authorization": f"Bearer {token}"} if token else {}
async def probe(name, path, use_auth=True):
"""One probe, its own short timeout, never raises."""
entry = {"path": path}
try:
async with _mk(timeout=8) as client:
r = await client.get(f"{worker}{path}",
headers=headers if use_auth else {})
entry["status"] = r.status_code
entry["body"] = r.text[:300]
except Exception as exc:
entry["error_type"] = type(exc).__name__
entry["error"] = repr(exc)[:200] or type(exc).__name__
out["probes"][name] = entry
return entry
# Run all probes concurrently so the endpoint returns in ~6s, not ~48s.
ver, _hours, ana, sch = await _aio.gather(
probe("version", "/version", use_auth=False),
probe("hours", "/hours", use_auth=False),
probe("analytics", "/analytics"),
probe("schema", "/analytics/schema"),
)
if request.query_params.get("test_write") == "1":
out["canary_note"] = "Use POST /admin/canary-run to run a canary. This diagnostic GET is read-only."
# Interpretation
if "error_type" in ver and "error_type" in out["probes"]["hours"]:
et = ver.get("error_type")
if et in ("ConnectTimeout", "ConnectError", "ReadTimeout"):
out["verdict"] = (
f"{et}: the HF server cannot open a connection to workers.dev. "
f"In v4.1 this legacy probe may continue to fail: the logged-in "
f"admin browser reads D1 with a 90-second token instead, and HF "
f"calls LibCal directly. Verify the dashboard rather than using "
f"this server-to-server probe as the acceptance test."
)
else:
out["verdict"] = (
f"No HTTP response from the Worker ({et}). Check that "
f"worker_url_used above is exactly your workers.dev address."
)
elif ver.get("status") == 404:
out["verdict"] = (
"Worker responds but has no /version route. Deploy the v4 secure gateway."
)
elif ana.get("status") == 401:
out["verdict"] = (
"Analytics returns 401. CLOUDFLARE_ADMIN_TOKEN does not match the "
"Worker's ADMIN_TOKEN (check for trailing spaces in either)."
)
elif ana.get("status") == 200:
migrated = None
if sch.get("status") == 200:
try:
import json as _json
migrated = (_json.loads(sch["body"]) or {}).get("migration_applied")
except Exception:
migrated = None
if migrated is False:
out["verdict"] = ("Connected, but the D1 migration is not applied "
"(answer_excerpt/source_ids missing).")
else:
out["verdict"] = "OK - backend, Worker and D1 analytics are connected."
else:
out["verdict"] = (f"Unexpected analytics status {ana.get('status')}. "
f"See probes for details.")
return out
@router.post("/canary-run")
async def admin_canary_run(request: Request):
"""Explicit admin action; never start a paid model request from a GET."""
_require_session(request)
settings = get_settings()
worker = _clean_worker_url(settings.cloudflare_worker_url)
if not worker or not settings.cloudflare_admin_token:
raise HTTPException(status_code=503, detail="Worker admin access is not configured")
from src.services.http_client import make_client
try:
async with make_client(timeout=190, retries=0) as client:
response = await client.post(
f"{worker}/canary/run",
headers={"Authorization": f"Bearer {settings.cloudflare_admin_token}"},
)
if response.status_code >= 400:
raise HTTPException(status_code=502, detail=f"Worker returned HTTP {response.status_code}")
return response.json()
except HTTPException:
raise
except Exception as exc:
logger.warning("Worker canary request failed: %s", type(exc).__name__)
raise HTTPException(status_code=502, detail="Worker canary unavailable") from exc
@router.get("/log-status")
async def admin_log_status(request: Request):
"""Outcome of recent analytics writes, without needing container logs.
'recent_attempts' is newest-first. Read it like this:
ok -> the write reached the Worker (HTTP 2xx)
throttled -> HTTP 403/429: the host is being rate-limited by Cloudflare
unreachable -> no HTTP response; exception type is in 'detail'
rejected_401 -> CLOUDFLARE_ADMIN_TOKEN does not match ADMIN_TOKEN
Buffered rows are retried automatically on later queries.
"""
_require_session(request)
from src.agentcore.orchestrator import analytics_log_status
from src.config import get_settings as _gs
st = _gs()
data = analytics_log_status()
data["worker_url_configured"] = bool(st.cloudflare_worker_url)
data["worker_token_configured"] = bool(st.cloudflare_admin_token)
if not data["recent_attempts"]:
data["hint"] = ("No write attempts recorded since the last restart. "
"Send a query through LibBee, then reload this page.")
return data
@router.get("/canary-status")
async def admin_canary_status(request: Request):
"""Last 14 daily canary runs (the Worker's scheduled health probe).
Each run is one real question through the full pipeline, chosen from a
fixed 14-question pool rotating by day-of-year. Canary traffic is excluded
from analytics, so these runs never appear in the D1 usage data.
"""
_require_session(request)
return await _worker_get("/canary/status")