Skip to content

Admin API

Base path /api/adminJWT required, and the caller must be an admin user (isAdmin: true). Covers platform administration (users, stats, audit log) and the F5 Control Plane — the web UI for driving the autonomous/manual learning loop that researches and publishes registry packets.

Users & platform stats

MethodPathPurpose
GET/api/admin/usersList all users with project/source/API-key counts
PUT/api/admin/users/:idUpdate a user (isAdmin, name)
DELETE/api/admin/users/:idDelete a user (cascades to their data)
GET/api/admin/statsPlatform-level counts (users, projects, sources, sessions)
GET/api/admin/audit-logSecurity-event audit trail (login, logout, admin promotion, etc.)

Notes:

  • PUT /api/admin/users/:id rejects isAdmin: false on your own account (400 Cannot remove your own admin status) — an admin can't accidentally lock themselves out.
  • DELETE /api/admin/users/:id rejects deleting your own account (400 Cannot delete your own account).
  • GET /api/admin/audit-log supports ?event= and ?userId= filters, plus ?limit= (default 50, capped 200) and ?offset=. Returns { entries, total, limit, offset }, newest first.

F5 Control Plane — the learning loop

The autonomous loop researches topics with Claude and publishes UKS packets to the registry. It can run on a 24/7 schedule (autoLearn, a live Redis-backed toggle) or be driven manually from these endpoints. Every packet the loop produces is published private (held for review) unless LEARNING_AUTOPUBLISH=on; an admin reviews and approves/rejects it here before it goes public.

MethodPathPurpose
GET/api/admin/learning/statusLoop config, topic counts by status, held-for-review count, recent failures
POST/api/admin/learning/modeFlip the 24/7 autoLearn toggle on/off live (no redeploy)
POST/api/admin/learning/seedSeed the starter topic genome into the worklist
POST/api/admin/learning/gap-detectionSeed graph-gap topics (EXP-46) into the worklist
POST/api/admin/learning/tickRun one research iteration right now
POST/api/admin/learning/research-topicResearch one specific topic on demand
POST/api/admin/learning/research-batchBatch-research N topics (AI-generated or graph-gap sourced), async
GET/api/admin/learning/review-queueList held (private) loop packets awaiting review
GET/api/admin/learning/packets/:idHeld-packet detail — sources + grades, for the review console
POST/api/admin/learning/packets/:id/approveApprove a held packet → public
POST/api/admin/learning/packets/:id/rejectReject a held packet (takedown; its topic re-queues)

GET /api/admin/learning/status

json
{
  "autoLearn": false,
  "learningMode": false,
  "scheduler": false,
  "autopublish": false,
  "dailyMaxCalls": 50,
  "topicTotal": 42,
  "topics": { "pending": 10, "researching": 0, "done": 28, "failed": 4 },
  "heldForReview": 2,
  "recentFailures": [
    { "topic": "…", "lastError": "…", "attempts": 3 }
  ]
}
  • autoLearn — the live toggle (Redis-backed via POST /learning/mode); this is what the scheduled learning-tick job actually checks each time it fires.
  • learningMode / scheduler / autopublish — the env-configured defaults (LEARNING_MODE, REGISTRY_SCHEDULER, LEARNING_AUTOPUBLISH) that seed autoLearn at boot and gate whether the repeatable jobs are registered at all.
  • dailyMaxCallsAI_DAILY_MAX_CALLS, the loop's daily AI-spend cap (null if unset).

POST /api/admin/learning/mode

Body: { "enabled": true }. Response: { "ok": true, "autoLearn": true }. Persisted in Redis (survives restarts; the LEARNING_MODE env var only seeds the initial default).

POST /api/admin/learning/research-topic

Research one topic immediately, regardless of the 24/7 toggle — an explicit operator action. Body: { "topic": "string (required)", "domain": "string (default 'general')" }. Upserts a top-priority SeedTopic row and runs one tick inline (which claims the highest-priority pending topic — i.e. the one just upserted). Response: { "topic": "…", ...tick result }.

POST /api/admin/learning/research-batch

Body: { "source": "ai" | "gaps", "count": 1-25 (default 3), "domain": "string (optional, 'ai' only)" }.

  • source: "ai" (default) — Claude proposes count novel topics (generateTopics).
  • source: "gaps"count graph weak-spots from EXP-46 gap detection.

Either way the topics are upserted into the worklist and count manual learning-ticks are enqueued (they bypass the 24/7 toggle; run in the background — results land in the review queue). Response: { "ok": true, "source": "ai", "seeded": 3, "queued": 3, "topics": [...] }.

GET /api/admin/learning/review-queue

?limit= (default 50, capped 200) &offset=. Returns { total, limit, offset, results: [{ packetId, topic, conformanceLevel, sourceCount, publishedAt, updatedAt }] } for packets that are authorKey === <the loop's internal actor key>, visibility: 'private', takenDown: false.

GET /api/admin/learning/packets/:id

Detail for one held packet — { packetId, topic, conformanceLevel, visibility, sources: [{ id, title, source_type, url, evidence_grade, clinical_status, credibility_score }] }. 404s if the packet doesn't exist or wasn't produced by the loop.

Approve / reject

  • Approve flips visibility to public and appends an approve entry to the tamper-evident audit log. A no-op ({ approved: true, note: 'already public' }) if already public; 403 if the packet was already rejected.
  • Reject sets takenDown: true (soft delete, same mechanism as admin takedown) and resets the originating SeedTopic back to pending (fresh attempts) so the loop researches it again instead of leaving it permanently stranded pointing at a taken-down packet.

Both endpoints are scoped to authorKey === <the loop's internal actor key> — an admin can never flip a user's own private packet public through these routes; that distinction is enforced server-side, not just by UI convention.

→ Related: Registry Control Plane overview · Trust & safety · Query the Registry

Released under the MIT License.