SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
36.3 KB · 562 lines python
Raw Blame History
1"""Admin API (`X-CA-Admin-Token`). Nothing is ever deleted: actions update `status` columns and append audit rows in payloads."""2from __future__ import annotations34from datetime import UTC, datetime5from typing import Any67from fastapi import APIRouter, Depends, HTTPException, Query, Response8from pydantic import BaseModel, Field910from companyatlas import archive11from companyatlas.api import aggregates as agg12from companyatlas.api import queries as q13from companyatlas.api import serializers as ser14from companyatlas.api.common import NO_STORE, PageDep, cache, page_payload, require_admin15from companyatlas.config import settings16from companyatlas.db import connection, execute, fetch_all, fetch_one, fetch_val, jsonb, transaction17from companyatlas.ids import new_id, slugify18from companyatlas.taxonomy import FailureClass, SensorStatus, tier_for_interval19from companyatlas.urls import canonicalize_url, registrable_domain2021ORDER = 1022router = APIRouter(prefix="/api/v1/admin", tags=["admin"], dependencies=[Depends(require_admin)])23LOW_QUALITY = 30.024STALE_DAYS = 725WORKER_WINDOW_MIN = 15262728def _ns(response: Response) -> None:29    response.headers["cache-control"] = NO_STORE303132# ------------------------------------------------------------------------------------------------ overview333435@router.get("/overview")36async def overview(response: Response) -> dict[str, Any]:37    _ns(response)38    today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)39    async with connection() as conn:40        cs = await fetch_all(conn, "select status, count(*) as n from companies group by status")41        cos = await fetch_all(conn, "select onboarding_status, count(*) as n from companies group by onboarding_status")42        ss = await fetch_all(conn, "select status, count(*) as n from sensors group by status")43        st = await fetch_all(conn, "select tier, count(*) as n from sensors where status <> 'retired' group by tier")44        qs = await fetch_one(conn, "select count(*) filter (where status = 'pending') as pending, count(*) filter (where status = 'running') as running, "45                                   "count(*) filter (where status = 'dead') as dead, count(*) filter (where status = 'failed') as failed, "46                                   "extract(epoch from (now() - min(run_at) filter (where status = 'pending' and run_at <= now()))) as oldest_pending_s from queue_jobs") or {}47        llm = await fetch_one(conn, "select count(*) filter (where status = 'pending') as pending, count(*) filter (where status = 'done' and finished_at >= :t) as done_today, "48                                    "count(*) filter (where status = 'failed' and finished_at >= :t) as failed_today from llm_jobs", t=today) or {}49        fails = await fetch_all(conn, "select failure_class, count(*) as n from failures where at >= now() - interval '24 hours' group by failure_class order by n desc")50        rates = await fetch_one(conn, "select (select count(*) from observations where fetched_at >= now() - interval '1 hour') as fetch_rate_1h, "51                                      "(select count(*) from changes where detected_at >= now() - interval '1 hour') as change_rate_1h, "52                                      "(select count(*) from changes where detected_at >= now() - interval '1 hour' and kind in ('meaningful','major','critical')) as meaningful_rate_1h") or {}53        workers = await fetch_all(conn, "select worker as name, max(fetched_at) as last_seen_at, count(*) as fetches_15m from observations "54                                        "where fetched_at >= now() - make_interval(mins => :m) and worker is not null group by worker order by last_seen_at desc limit 50",55                                  m=WORKER_WINDOW_MIN)56        inflight = {r["claimed_by"]: int(r["n"]) for r in await fetch_all(conn, "select claimed_by, count(*) as n from sensors where claimed_by is not null group by claimed_by")}57        runs = await fetch_all(conn, "select worker as name, max(started_at) as last_seen_at from crawl_runs where started_at >= now() - make_interval(mins => :m) "58                                     "and worker is not null group by worker", m=WORKER_WINDOW_MIN)59        cost = await fetch_all(conn, "select dimension, sum(cost_estimate) as cost, sum(units) as units from cost_ledger where day = current_date group by dimension")60        periodic = await q.settings_value(conn, "scheduler:heartbeat")61    names = {w["name"] for w in workers} | {r["name"] for r in runs} | set(inflight)62    seen = {w["name"]: w["last_seen_at"] for w in workers}63    for r in runs:64        if r["name"] not in seen or (r["last_seen_at"] and seen[r["name"]] and r["last_seen_at"] > seen[r["name"]]):65            seen[r["name"]] = r["last_seen_at"]66    done_today, failed_today = int(llm.get("done_today") or 0), int(llm.get("failed_today") or 0)67    costs = {r["dimension"]: round(float(r["cost"] or 0), 4) for r in cost}68    return {"companies_by_status": {r["status"]: int(r["n"]) for r in cs}, "companies_by_onboarding": {r["onboarding_status"]: int(r["n"]) for r in cos},69            "sensors_by_status": {r["status"]: int(r["n"]) for r in ss}, "sensors_by_tier": {(r["tier"] or "").strip(): int(r["n"]) for r in st},70            "queue": {"pending": int(qs.get("pending") or 0), "running": int(qs.get("running") or 0), "dead": int(qs.get("dead") or 0),71                      "failed": int(qs.get("failed") or 0), "oldest_pending_s": round(float(qs["oldest_pending_s"]), 1) if qs.get("oldest_pending_s") is not None else 0.0},72            "llm": {"pending": int(llm.get("pending") or 0), "done_today": done_today, "failed_today": failed_today,73                    "budget_left": max(0, settings.llm_daily_budget - done_today - failed_today), "budget": settings.llm_daily_budget, "configured": settings.llm_configured},74            "failures_24h_by_class": {r["failure_class"]: int(r["n"]) for r in fails}, "fetch_rate_1h": int(rates.get("fetch_rate_1h") or 0),75            "change_rate_1h": int(rates.get("change_rate_1h") or 0), "meaningful_rate_1h": int(rates.get("meaningful_rate_1h") or 0),76            "storage": await agg.archive_stats(),77            "workers": [{"name": n, "last_seen_at": seen.get(n), "inflight": inflight.get(n, 0)} for n in sorted(names)],78            "cost_today": {"fetch": costs.get("fetch", 0.0), "browser": costs.get("browser", 0.0), "llm": costs.get("llm", 0.0), "total": round(sum(costs.values()), 4)},79            "scheduler_heartbeat": periodic, "time": datetime.now(UTC)}808182# ------------------------------------------------------------------------------------------------ connectors838485@router.get("/connectors")86async def connectors(response: Response) -> dict[str, Any]:87    _ns(response)88    async with connection() as conn:89        rows = await fetch_all(conn, "select * from connectors order by category, id")90        sensors = await fetch_all(conn, "select connector_id, count(*) filter (where status = 'active') as active, "91                                        "count(*) filter (where status in ('failing','stale','blocked')) as failing, count(*) filter (where status <> 'retired') as total, "92                                        "max(last_run_at) as last_run_at from sensors group by connector_id")93        obs = await fetch_all(conn, "select s.connector_id, count(*) as n, count(*) filter (where o.failure_class is null) as ok, avg(o.duration_ms) as latency, "94                                    "count(*) filter (where o.changed) as changed, count(*) filter (where o.failure_class is not null) as errors "95                                    "from observations o join sensors s on s.id = o.sensor_id where o.fetched_at >= now() - interval '24 hours' group by s.connector_id")96    sm = {r["connector_id"]: r for r in sensors}97    om = {r["connector_id"]: r for r in obs}98    items = []99    known = {r["id"] for r in rows}100    for cid in list(known) + [k for k in sm if k not in known]:101        row = next((r for r in rows if r["id"] == cid), None)102        base = ser.connector(row) if row else {"id": cid, "name": cid, "version": "?", "category": "?", "enabled": True}103        s, o = sm.get(cid, {}), om.get(cid, {})104        n = int(o.get("n") or 0)105        base.update({"sensors_active": int(s.get("active") or 0), "sensors_failing": int(s.get("failing") or 0), "sensors_total": int(s.get("total") or 0),106                     "success_rate_24h": round(int(o.get("ok") or 0) / n, 4) if n else None, "avg_latency_ms": round(float(o["latency"]), 1) if o.get("latency") is not None else None,107                     "change_rate_24h": round(int(o.get("changed") or 0) / n, 4) if n else None, "errors_24h": int(o.get("errors") or 0), "fetches_24h": n,108                     "last_run_at": s.get("last_run_at")})109        items.append(base)110    items.sort(key=lambda x: (x.get("category") or "", x["id"]))111    return {"items": items}112113114# ------------------------------------------------------------------------------------------------ sensors115116SENSOR_FILTERS: dict[str, str] = {117    "healthy": "s.status = 'active' and s.consecutive_failures = 0",118    "failing": "(s.status = 'failing' or s.consecutive_failures > 0)",119    "stale": "(s.status = 'stale' or (s.status in ('active','failing') and (s.last_success_at is null or s.last_success_at < now() - make_interval(days => :stale_days))))",120    "blocked": "(s.status = 'blocked' or s.last_failure_class in ('BOT_CHALLENGE','ROBOTS','BLOCKED_DESTINATION','RATE_LIMIT'))",121    "redirected": "(s.status = 'redirected' or s.last_failure_class = 'REDIRECT')",122    "low_quality": "s.quality_score < :low_quality",123    "high_activity": "s.last_change_at >= now() - interval '24 hours'",124}125SENSOR_SORTS = {"recent": "s.last_run_at desc nulls last, s.id", "next_run": "s.next_run_at asc, s.id", "failures": "s.consecutive_failures desc, s.last_run_at desc nulls last, s.id",126                "quality": "s.quality_score asc, s.id", "changes": "s.change_count desc, s.id", "created": "s.created_at desc, s.id"}127SENSOR_SELECT = ("select s.*, c.slug as company_slug, c.display_name as company_display_name, c.canonical_domain as company_domain, c.country as company_country, "128                 "c.logo_url as company_logo_url from sensors s join companies c on c.id = s.company_id")129130131@router.get("/sensors")132async def admin_sensors(response: Response, p: PageDep, status: str | None = None, domain: str | None = None, connector: str | None = None,133                        company: str | None = None, surface: str | None = None,134                        filter: str | None = Query(None, pattern="^(healthy|failing|stale|blocked|redirected|low_quality|high_activity)$"),135                        sort: str = Query("recent", pattern="^(recent|next_run|failures|quality|changes|created)$")) -> dict[str, Any]:136    _ns(response)137    where, params = ["true"], {"stale_days": STALE_DAYS, "low_quality": LOW_QUALITY}138    if status:139        where.append("s.status = cast(:status as text)")140        params["status"] = status.lower()[:20]141    if domain:142        where.append("s.domain = cast(:domain as text)")143        params["domain"] = domain.lower()[:253]144    if connector:145        where.append("s.connector_id = cast(:connector as text)")146        params["connector"] = connector[:80]147    if surface:148        where.append("s.surface = cast(:surface as text)")149        params["surface"] = surface.lower()[:40]150    if filter:151        where.append(SENSOR_FILTERS[filter])152        if filter == "high_activity":153            sort = "changes"154    async with connection() as conn:155        if company:156            params["company_id"] = (await q.require_company(conn, company))["id"]157            where.append("s.company_id = :company_id")158        wsql = " and ".join(where)159        rows = await fetch_all(conn, f"{SENSOR_SELECT} where {wsql} order by {SENSOR_SORTS[sort]} limit :limit offset :offset", **params, limit=p.per_page, offset=p.offset)160        total = await q.bounded_count(conn, f"from sensors s join companies c on c.id = s.company_id where {wsql}", params)161    items = []162    for r in rows:163        item = ser.sensor_admin(r)164        item["company"] = ser.company_ref(r)165        items.append(item)166    return page_payload(items, total, p)167168169class SensorActionBody(BaseModel):170    interval_s: int | None = Field(None, ge=60, le=90 * 86400)171    connector_id: str | None = Field(None, max_length=80)172    reason: str | None = Field(None, max_length=500)173174175SENSOR_ACTIONS = ("pause", "resume", "retry", "rediscover", "retire", "run_now", "set_interval", "set_connector")176177178@router.post("/sensors/{sensor_id}/{action}")179async def sensor_action(sensor_id: str, action: str, response: Response, body: SensorActionBody | None = None) -> dict[str, Any]:180    _ns(response)181    if action not in SENSOR_ACTIONS:182        raise HTTPException(status_code=404, detail=f"unknown action (one of {', '.join(SENSOR_ACTIONS)})")183    body = body or SensorActionBody()184    now = datetime.now(UTC)185    async with transaction() as conn:186        s = await fetch_one(conn, "select * from sensors where id = :id", id=sensor_id)187        if s is None:188            raise HTTPException(status_code=404, detail="sensor not found")189        extra: dict[str, Any] = {}190        if action == "pause":191            await execute(conn, "update sensors set status = :st, claimed_by = null, claimed_at = null, updated_at = :now where id = :id", st=SensorStatus.PAUSED.value, now=now, id=sensor_id)192        elif action == "resume":193            await execute(conn, "update sensors set status = :st, consecutive_failures = 0, next_run_at = :now, retired_at = null, updated_at = :now where id = :id",194                          st=SensorStatus.ACTIVE.value, now=now, id=sensor_id)195        elif action in ("retry", "run_now"):196            prio = ", priority = greatest(priority, 0.95)" if action == "run_now" else ""197            await execute(conn, f"update sensors set next_run_at = :now, claimed_by = null, claimed_at = null{prio}, updated_at = :now where id = :id", now=now, id=sensor_id)198        elif action == "retire":199            await execute(conn, "update sensors set status = :st, retired_at = :now, claimed_by = null, claimed_at = null, updated_at = :now where id = :id",200                          st=SensorStatus.RETIRED.value, now=now, id=sensor_id)201        elif action == "rediscover":202            jid = new_id("queue_job")203            key = f"discover:{s['company_id']}:{int(now.timestamp())}"204            await execute(conn, "insert into queue_jobs (id, kind, key, payload, priority) values (:id, 'discover', :key, cast(:p as jsonb), 0.9) on conflict (key) do nothing",205                          id=jid, key=key, p=jsonb({"company_id": s["company_id"], "sensor_id": sensor_id, "reason": body.reason or "admin:rediscover"}))206            extra["queued"] = {"id": jid, "key": key}207        elif action == "set_interval":208            if body.interval_s is None:209                raise HTTPException(status_code=422, detail="interval_s required")210            iv = max(settings.min_interval_s, min(settings.max_interval_s, body.interval_s))211            await execute(conn, "update sensors set base_interval_s = :iv, current_interval_s = :iv, tier = :tier, next_run_at = :now, updated_at = :now where id = :id",212                          iv=iv, tier=tier_for_interval(iv), now=now, id=sensor_id)213            extra["interval_s"] = iv214        elif action == "set_connector":215            if not body.connector_id:216                raise HTTPException(status_code=422, detail="connector_id required")217            ok = await fetch_val(conn, "select enabled from connectors where id = :c", c=body.connector_id)218            if ok is None:219                raise HTTPException(status_code=422, detail="unknown connector_id")220            await execute(conn, "update sensors set connector_id = :c, next_run_at = :now, updated_at = :now where id = :id", c=body.connector_id, now=now, id=sensor_id)221        row = await fetch_one(conn, f"{SENSOR_SELECT} where s.id = :id", id=sensor_id)222    out = ser.sensor_admin(row or {})223    out["company"] = ser.company_ref(row or {})224    return {"ok": True, "action": action, "sensor": out, **extra}225226227# ------------------------------------------------------------------------------------------------ companies228229230@router.get("/companies")231async def admin_companies(response: Response, p: PageDep, onboarding_status: str | None = None, status: str | None = None,232                          q_: str | None = Query(None, alias="q", max_length=200), country: str | None = None,233                          sort: str = Query("recent", pattern="^(activity|events|hiring|name|importance|recent)$")) -> dict[str, Any]:234    _ns(response)235    where, params = q.company_filters(q=q_, country=country, status=status, onboarding_status=onboarding_status)236    async with connection() as conn:237        ids, total = await q.company_page_ids(conn, where, params, sort=sort, limit=p.per_page, offset=p.offset)238        rows = await q.fetch_cards_by_ids(conn, ids)239    items = []240    for r in rows:241        card = ser.company_card(r)242        card.update({"onboarding_error": r.get("onboarding_error"), "indexed": bool(r.get("indexed")), "discovered_at": r.get("discovered_at"), "updated_at": r.get("updated_at")})243        items.append(card)244    return page_payload(items, total, p)245246247class CompanyCreate(BaseModel):248    website: str = Field(min_length=4, max_length=500)249    display_name: str | None = Field(None, max_length=200)250    country: str | None = Field(None, min_length=2, max_length=2)251    industries: list[str] | None = None252    importance: float | None = Field(None, ge=0, le=1)253    tier: int | None = Field(None, ge=1, le=4)254255256@router.post("/companies", status_code=201)257async def admin_create_company(body: CompanyCreate, response: Response) -> dict[str, Any]:258    _ns(response)259    website = body.website.strip()260    if "://" not in website:261        website = "https://" + website262    if not website.lower().startswith(("http://", "https://")):263        raise HTTPException(status_code=422, detail="website: only http(s) URLs are accepted")264    domain = registrable_domain(website)265    if not domain or "." not in domain:266        raise HTTPException(status_code=422, detail="website: could not derive a registrable domain")267    website = canonicalize_url(website)268    display_name = (body.display_name or "").strip() or domain.split(".")[0].capitalize()269    country = body.country.upper() if body.country else None270    industries = [slugify(i) for i in (body.industries or []) if i.strip()][:10]271    async with transaction() as conn:272        existing = await fetch_one(conn, "select id, slug from companies where canonical_domain = :d", d=domain)273        if existing:274            raise HTTPException(status_code=409, detail=f"company already exists: {existing['slug']}")275        if country and not await fetch_val(conn, "select 1 from countries where code = cast(:c as char(2))", c=country):276            raise HTTPException(status_code=422, detail="country: unknown ISO-2 code")277        if industries:278            known = {r["slug"] for r in await fetch_all(conn, "select slug from industries where slug = any(cast(:s as text[]))", s=industries)}279            missing = [i for i in industries if i not in known]280            if missing:281                raise HTTPException(status_code=422, detail=f"industries: unknown slugs {', '.join(missing)}")282        base = slugify(display_name)283        slug, n = base, 2284        while await fetch_val(conn, "select 1 from companies where slug = :s", s=slug):285            slug = f"{base}-{n}"286            n += 1287        cid = new_id("company")288        await execute(conn, "insert into companies (id, slug, display_name, canonical_domain, website, country, industries, industry_primary, importance, tier, "289                            "source_meta) values (:id, :slug, :name, :domain, :website, :country, cast(:inds as text[]), :ip, :imp, :tier, cast(:meta as jsonb))",290                      id=cid, slug=slug, name=display_name, domain=domain, website=website, country=country, inds=industries, ip=industries[0] if industries else None,291                      imp=body.importance if body.importance is not None else 0.2, tier=body.tier or 4, meta=jsonb({"source": "admin_api", "created_at": datetime.now(UTC)}))292        await execute(conn, "insert into domains (id, company_id, domain, kind) values (:id, :cid, :d, 'primary') on conflict do nothing", id=new_id("domain"), cid=cid, d=domain)293        jid = new_id("queue_job")294        await execute(conn, "insert into queue_jobs (id, kind, key, payload, priority) values (:id, 'discover', :key, cast(:p as jsonb), 0.8) on conflict (key) do nothing",295                      id=jid, key=f"discover:{cid}", p=jsonb({"company_id": cid, "reason": "admin:create"}))296        cards = await q.fetch_cards_by_ids(conn, [cid])297    return {"ok": True, "company": ser.company_card(cards[0]), "queued": {"id": jid, "kind": "discover"}}298299300@router.post("/companies/{key}/rediscover")301async def admin_rediscover(key: str, response: Response) -> dict[str, Any]:302    _ns(response)303    now = datetime.now(UTC)304    async with transaction() as conn:305        c = await q.require_company(conn, key)306        jid = new_id("queue_job")307        jkey = f"discover:{c['id']}:{int(now.timestamp())}"308        await execute(conn, "insert into queue_jobs (id, kind, key, payload, priority) values (:id, 'discover', :key, cast(:p as jsonb), 0.9)",309                      id=jid, key=jkey, p=jsonb({"company_id": c["id"], "reason": "admin:rediscover"}))310        if c["onboarding_status"] in ("failed", "no_website"):311            await execute(conn, "update companies set onboarding_status = 'pending', onboarding_error = null, updated_at = :now where id = :id", now=now, id=c["id"])312    return {"ok": True, "company": c["slug"], "queued": {"id": jid, "key": jkey}}313314315# ------------------------------------------------------------------------------------------------ failures / queue / llm / reviews316317318@router.get("/failures")319async def admin_failures(response: Response, p: PageDep, class_: str | None = Query(None, alias="class", max_length=40), since: str | None = None,320                         sensor: str | None = None, company: str | None = None) -> dict[str, Any]:321    _ns(response)322    where, params = ["true"], {}323    if class_:324        where.append("f.failure_class = cast(:fc as text)")325        params["fc"] = class_.upper()326    since_dt = q.parse_iso(since) or q.days_ago(7)327    where.append("f.at >= :since")328    params["since"] = since_dt329    if sensor:330        where.append("f.sensor_id = :sid")331        params["sid"] = sensor332    async with connection() as conn:333        if company:334            params["cid"] = (await q.require_company(conn, company))["id"]335            where.append("f.company_id = :cid")336        wsql = " and ".join(where)337        rows = await fetch_all(conn, f"select f.*, c.slug as company_slug, s.surface from failures f left join companies c on c.id = f.company_id "338                                     f"left join sensors s on s.id = f.sensor_id where {wsql} order by f.at desc limit :limit offset :offset", **params, limit=p.per_page, offset=p.offset)339        total = await q.bounded_count(conn, f"from failures f where {wsql}", params)340        by_class = await fetch_all(conn, f"select f.failure_class, count(*) as n from failures f where {wsql} group by 1 order by n desc", **params)341    out = page_payload([ser.failure(r) for r in rows], total, p)342    out["by_class"] = {r["failure_class"]: int(r["n"]) for r in by_class}343    out["classes"] = [c.value for c in FailureClass]344    return out345346347@router.get("/queue")348async def admin_queue(response: Response, kind: str | None = None, status: str | None = None, limit: int = Query(100, ge=1, le=500)) -> dict[str, Any]:349    _ns(response)350    where, params = ["true"], {"limit": limit}351    if kind:352        where.append("kind = cast(:kind as text)")353        params["kind"] = kind[:40]354    if status:355        where.append("status = cast(:status as text)")356        params["status"] = status[:20]357    async with connection() as conn:358        counts = await fetch_all(conn, "select kind, status, count(*) as n from queue_jobs group by kind, status order by kind, status")359        rows = await fetch_all(conn, f"select * from queue_jobs where {' and '.join(where)} order by case status when 'running' then 0 when 'pending' then 1 else 2 end, "360                                     "run_at desc limit :limit", **params)361    return {"counts": [{"kind": r["kind"], "status": r["status"], "n": int(r["n"])} for r in counts], "items": [ser.queue_job(r) for r in rows]}362363364class RequeueBody(BaseModel):365    kind: str | None = Field(None, max_length=40)366367368@router.post("/queue/requeue-dead")369async def admin_requeue_dead(response: Response, body: RequeueBody | None = None) -> dict[str, Any]:370    _ns(response)371    body = body or RequeueBody()372    extra, params = "", {}373    if body.kind:374        extra = " and kind = cast(:kind as text)"375        params["kind"] = body.kind376    async with transaction() as conn:377        n = await fetch_val(conn, "with u as (update queue_jobs set status = 'pending', attempts = 0, run_at = now(), locked_at = null, locked_by = null, "378                                  f"last_error = null, finished_at = null where status = 'dead'{extra} returning 1) select count(*) from u", **params)379    return {"ok": True, "requeued": int(n or 0)}380381382@router.get("/llm")383async def admin_llm(response: Response, p: PageDep, status: str | None = None, kind: str | None = None) -> dict[str, Any]:384    _ns(response)385    where, params = ["true"], {}386    if status:387        where.append("status = cast(:status as text)")388        params["status"] = status[:20]389    if kind:390        where.append("kind = cast(:kind as text)")391        params["kind"] = kind[:40]392    wsql = " and ".join(where)393    async with connection() as conn:394        rows = await fetch_all(conn, f"select * from llm_jobs where {wsql} order by created_at desc limit :limit offset :offset", **params, limit=p.per_page, offset=p.offset)395        total = await q.bounded_count(conn, f"from llm_jobs where {wsql}", params)396        stats = await fetch_one(conn, "select count(*) filter (where status = 'pending') as pending, count(*) filter (where status = 'running') as running, "397                                      "count(*) filter (where status = 'done' and finished_at >= current_date) as done_today, "398                                      "count(*) filter (where status = 'failed' and finished_at >= current_date) as failed_today, "399                                      "avg(latency_ms) filter (where status = 'done' and finished_at >= current_date) as avg_latency_ms, "400                                      "sum(request_tokens + coalesce(response_tokens, 0)) filter (where finished_at >= current_date) as tokens_today from llm_jobs") or {}401    out = page_payload([ser.llm_job(r) for r in rows], total, p)402    out["stats"] = {k: (round(float(v), 1) if k == "avg_latency_ms" and v is not None else (int(v) if v is not None else 0)) for k, v in stats.items()}403    out["stats"]["budget"] = settings.llm_daily_budget404    out["stats"]["configured"] = settings.llm_configured405    return out406407408@router.get("/reviews")409async def admin_reviews(response: Response, p: PageDep, kind: str | None = None, status: str = Query("open", max_length=20)) -> dict[str, Any]:410    _ns(response)411    where, params = ["true"], {}412    if kind:413        where.append("r.kind = cast(:kind as text)")414        params["kind"] = kind[:40]415    if status and status != "all":416        where.append("r.status = cast(:status as text)")417        params["status"] = status418    wsql = " and ".join(where)419    async with connection() as conn:420        rows = await fetch_all(conn, f"select r.*, c.slug as company_slug, c.display_name as company_display_name from review_queue r left join companies c on c.id = r.company_id "421                                     f"where {wsql} order by r.created_at asc limit :limit offset :offset", **params, limit=p.per_page, offset=p.offset)422        total = await q.bounded_count(conn, f"from review_queue r where {wsql}", params)423        by_kind = await fetch_all(conn, "select kind, count(*) as n from review_queue where status = 'open' group by kind order by n desc")424    out = page_payload([ser.review(r) for r in rows], total, p)425    out["open_by_kind"] = {r["kind"]: int(r["n"]) for r in by_kind}426    return out427428429class ReviewBody(BaseModel):430    resolution: str = Field(pattern="^(accepted|rejected)$")431    note: str | None = Field(None, max_length=1000)432    label: str | None = Field(None, pattern="^(correct|duplicate|noise|misclassified)$")433434435@router.post("/reviews/{review_id}")436async def admin_resolve_review(review_id: str, body: ReviewBody, response: Response) -> dict[str, Any]:437    _ns(response)438    async with transaction() as conn:439        r = await fetch_one(conn, "select * from review_queue where id = :id", id=review_id)440        if r is None:441            raise HTTPException(status_code=404, detail="review not found")442        if r["status"] != "open":443            raise HTTPException(status_code=409, detail=f"review already {r['status']}")444        payload = ser._dict(r["payload"])445        payload["resolution"] = {"status": body.resolution, "note": body.note, "label": body.label, "at": datetime.now(UTC)}446        await execute(conn, "update review_queue set status = :st, resolution = :res, resolved_at = now(), payload = cast(:p as jsonb) where id = :id",447                      st=body.resolution, res=body.label or body.note or body.resolution, p=jsonb(payload), id=review_id)448        row = await fetch_one(conn, "select * from review_queue where id = :id", id=review_id)449    return {"ok": True, "review": ser.review(row or {})}450451452# ------------------------------------------------------------------------------------------------ events (corrections)453454455class RetractBody(BaseModel):456    reason: str = Field(min_length=3, max_length=500)457458459async def _audit(conn: Any, event_id: str, action: str, reason: str | None) -> None:460    entry = jsonb([{"action": action, "reason": reason, "at": datetime.now(UTC)}])461    await execute(conn, "update events set payload = jsonb_set(payload, '{_audit}', coalesce(payload->'_audit', '[]'::jsonb) || cast(:e as jsonb), true) where id = :id",462                  e=entry, id=event_id)463464465@router.post("/events/{event_id}/retract")466async def admin_retract_event(event_id: str, body: RetractBody, response: Response) -> dict[str, Any]:467    _ns(response)468    async with transaction() as conn:469        ev = await fetch_one(conn, "select id, status from events where id = :id", id=event_id)470        if ev is None:471            raise HTTPException(status_code=404, detail="event not found")472        await execute(conn, "update events set status = 'retracted', retracted_reason = :r where id = :id", r=body.reason.strip(), id=event_id)473        await _audit(conn, event_id, "retract", body.reason.strip())474        row = await q.fetch_event(conn, event_id)475    cache.clear()476    return {"ok": True, "event": ser.event(row or {})}477478479@router.post("/events/{event_id}/restore")480async def admin_restore_event(event_id: str, response: Response) -> dict[str, Any]:481    _ns(response)482    async with transaction() as conn:483        ev = await fetch_one(conn, "select id, status, retracted_reason from events where id = :id", id=event_id)484        if ev is None:485            raise HTTPException(status_code=404, detail="event not found")486        await execute(conn, "update events set status = 'active', retracted_reason = null where id = :id", id=event_id)487        await _audit(conn, event_id, "restore", ev.get("retracted_reason"))488        row = await q.fetch_event(conn, event_id)489    cache.clear()490    return {"ok": True, "event": ser.event(row or {})}491492493# ------------------------------------------------------------------------------------------------ quality / costs / cache494495496@router.get("/quality")497async def admin_quality(response: Response) -> dict[str, Any]:498    _ns(response)499    async with connection() as conn:500        r = await fetch_one(conn, """501            select (select count(*) from companies) as companies,502                   (select count(*) from companies where onboarding_status = 'active' and status = 'ACTIVE') as companies_active,503                   (select count(*) from sensors where status <> 'retired') as sensors,504                   (select count(*) from sensors where status = 'active') as sensors_active,505                   (select count(*) from sensors where status <> 'retired' and last_run_at >= now() - interval '24 hours') as checked_24h,506                   (select count(*) from sensors where status = 'stale') as stale,507                   (select count(*) from sensors where status in ('failing', 'blocked')) as failed_sensors,508                   (select count(*) from sensors where surface = 'other' and status <> 'retired') as unknown_surfaces,509                   (select count(*) from events where detected_at >= now() - interval '30 days') as events_30d,510                   (select count(*) from events where detected_at >= now() - interval '30 days' and status = 'duplicate') as duplicates_30d,511                   (select avg(confidence) from events where detected_at >= now() - interval '30 days' and status = 'active') as confidence_avg512        """) or {}513        cal = await fetch_all(conn, "select coalesce(payload->'resolution'->>'label', resolution) as label, count(*) as n from review_queue "514                                    "where status in ('accepted','rejected','resolved') group by 1")515    companies, sensors = int(r.get("companies") or 0), int(r.get("sensors") or 0)516    ev30 = int(r.get("events_30d") or 0)517    labels = {c["label"]: int(c["n"]) for c in cal if c["label"]}518    return {"coverage": {"companies_active_pct": round(int(r.get("companies_active") or 0) / companies * 100, 1) if companies else None,519                         "sensors_active_pct": round(int(r.get("sensors_active") or 0) / sensors * 100, 1) if sensors else None,520                         "companies": companies, "sensors": sensors},521            "freshness": {"sensors_checked_24h_pct": round(int(r.get("checked_24h") or 0) / sensors * 100, 1) if sensors else None, "stale": int(r.get("stale") or 0)},522            "duplicate_rate": round(int(r.get("duplicates_30d") or 0) / ev30, 4) if ev30 else None,523            "event_confidence_avg": round(float(r["confidence_avg"]), 3) if r.get("confidence_avg") is not None else None,524            "unknown_surfaces": int(r.get("unknown_surfaces") or 0), "failed_sensors": int(r.get("failed_sensors") or 0),525            "calibration": {k: labels.get(k, 0) for k in ("correct", "duplicate", "noise", "misclassified")}, "events_30d": ev30}526527528@router.get("/costs")529async def admin_costs(response: Response, days: int = Query(30, ge=1, le=365)) -> dict[str, Any]:530    _ns(response)531    async with connection() as conn:532        items = await fetch_all(conn, "select day, dimension, key, units, cost_estimate from cost_ledger where day >= :d order by day desc, dimension, key limit 5000",533                                d=q.days_ago(days).date())534        denom = await fetch_one(conn, "select (select count(*) from companies where status = 'ACTIVE') as companies, "535                                      "(select count(*) from observations where fetched_at >= :d) as observations, "536                                      "(select count(*) from events where status = 'active' and detected_at >= :d and importance >= :imp) as meaningful_events",537                                d=q.days_ago(days), imp=settings.meaningful_threshold) or {}538    total = sum(float(i["cost_estimate"] or 0) for i in items)539    by_dim: dict[str, float] = {}540    for i in items:541        by_dim[i["dimension"]] = by_dim.get(i["dimension"], 0.0) + float(i["cost_estimate"] or 0)542    comp, obs, mev = int(denom.get("companies") or 0), int(denom.get("observations") or 0), int(denom.get("meaningful_events") or 0)543    return {"days": days, "items": [{"day": i["day"], "dimension": i["dimension"], "key": i["key"], "units": float(i["units"] or 0), "cost_estimate": float(i["cost_estimate"] or 0)}544                                    for i in items],545            "total": round(total, 4), "by_dimension": {k: round(v, 4) for k, v in by_dim.items()},546            "per_1000_companies": round(total / comp * 1000, 4) if comp else None, "per_million_observations": round(total / obs * 1_000_000, 4) if obs else None,547            "per_meaningful_event": round(total / mev, 4) if mev else None}548549550@router.post("/cache/clear")551async def admin_cache_clear(response: Response, prefix: str | None = Query(None, max_length=60)) -> dict[str, Any]:552    _ns(response)553    cache.clear(prefix)554    return {"ok": True, "cleared": prefix or "all"}555556557@router.get("/storage")558async def admin_storage(response: Response) -> dict[str, Any]:559    _ns(response)560    stats = await agg.archive_stats()561    return {"objects_dir": str(settings.objects_dir), "exists": archive.object_path("00" * 32).parent.parent.parent.exists(), **stats}562