Admin API
Base path /api/admin — JWT 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
| Method | Path | Purpose |
|---|---|---|
| GET | /api/admin/users | List all users with project/source/API-key counts |
| PUT | /api/admin/users/:id | Update a user (isAdmin, name) |
| DELETE | /api/admin/users/:id | Delete a user (cascades to their data) |
| GET | /api/admin/stats | Platform-level counts (users, projects, sources, sessions) |
| GET | /api/admin/audit-log | Security-event audit trail (login, logout, admin promotion, etc.) |
Notes:
PUT /api/admin/users/:idrejectsisAdmin: falseon your own account (400 Cannot remove your own admin status) — an admin can't accidentally lock themselves out.DELETE /api/admin/users/:idrejects deleting your own account (400 Cannot delete your own account).GET /api/admin/audit-logsupports?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.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/admin/learning/status | Loop config, topic counts by status, held-for-review count, recent failures |
| POST | /api/admin/learning/mode | Flip the 24/7 autoLearn toggle on/off live (no redeploy) |
| POST | /api/admin/learning/seed | Seed the starter topic genome into the worklist |
| POST | /api/admin/learning/gap-detection | Seed graph-gap topics (EXP-46) into the worklist |
| POST | /api/admin/learning/tick | Run one research iteration right now |
| POST | /api/admin/learning/research-topic | Research one specific topic on demand |
| POST | /api/admin/learning/research-batch | Batch-research N topics (AI-generated or graph-gap sourced), async |
| GET | /api/admin/learning/review-queue | List held (private) loop packets awaiting review |
| GET | /api/admin/learning/packets/:id | Held-packet detail — sources + grades, for the review console |
| POST | /api/admin/learning/packets/:id/approve | Approve a held packet → public |
| POST | /api/admin/learning/packets/:id/reject | Reject a held packet (takedown; its topic re-queues) |
GET /api/admin/learning/status
{
"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 viaPOST /learning/mode); this is what the scheduledlearning-tickjob actually checks each time it fires.learningMode/scheduler/autopublish— the env-configured defaults (LEARNING_MODE,REGISTRY_SCHEDULER,LEARNING_AUTOPUBLISH) that seedautoLearnat boot and gate whether the repeatable jobs are registered at all.dailyMaxCalls—AI_DAILY_MAX_CALLS, the loop's daily AI-spend cap (nullif 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 proposescountnovel topics (generateTopics).source: "gaps"—countgraph 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
visibilitytopublicand appends anapproveentry to the tamper-evident audit log. A no-op ({ approved: true, note: 'already public' }) if already public;403if the packet was already rejected. - Reject sets
takenDown: true(soft delete, same mechanism as admin takedown) and resets the originatingSeedTopicback topending(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