pdf-quiz-generator/docs/api-reference.md
Daniel fdebda993c
Some checks failed
Mobile Android Release / android-release (push) Failing after 1s
Improve quiz TTS and Android release build
2026-05-12 01:01:01 +02:00

1051 lines
27 KiB
Markdown

# PedsHub API Reference
Base URL: `/api`
All endpoints return JSON unless noted otherwise. Authentication is via Bearer token in the `Authorization` header. Role hierarchy: **admin > moderator > user**.
---
## Auth (`/api/auth`)
### POST `/api/auth/register`
Register a new user account.
- **Auth:** None
- **Rate limit:** Turnstile verification required if configured
- **Request body:**
```json
{
"email": "string",
"password": "string (min 8 chars)",
"name": "string",
"turnstile_token": "string | null"
}
```
- **Response:**
- First user ever: `{"access_token": "...", "token_type": "bearer"}` (auto-admin, auto-verified)
- Subsequent users: `{"requires_verification": true, "message": "..."}`
- **Notes:** Registration can be disabled by admin via settings. First registered user is always admin and auto-verified.
### POST `/api/auth/login`
Authenticate and receive a JWT token.
- **Auth:** None
- **Rate limit:** 10 attempts per IP per 15 minutes (Redis). Turnstile verification if configured.
- **Request body:**
```json
{
"email": "string",
"password": "string",
"turnstile_token": "string | null"
}
```
- **Response:** `{"access_token": "string", "token_type": "bearer"}`
- **Errors:** 401 invalid credentials, 403 email not verified, 429 rate limited
### GET `/api/auth/verify-email`
Verify a user's email address via token link.
- **Auth:** None
- **Query params:** `token` (string, required)
- **Response:** `{"message": "Email verified successfully! You can now log in."}`
- **Errors:** 400 invalid/expired token
### POST `/api/auth/resend-verification`
Resend the email verification link.
- **Auth:** None
- **Request body:** `{"email": "string"}`
- **Response:** `{"message": "If that email exists, a verification link has been sent."}`
- **Notes:** Deliberately vague response to prevent email enumeration.
### POST `/api/auth/forgot-password`
Request a password reset link.
- **Auth:** None
- **Rate limit:** 3 reset requests per email per hour
- **Request body:** `{"email": "string"}`
- **Response:** `{"message": "If that email is registered, a reset link has been sent."}`
- **Notes:** Deliberately vague response to prevent email enumeration. Sends email in background.
### POST `/api/auth/reset-password`
Reset password using a valid token.
- **Auth:** None
- **Request body:**
```json
{
"token": "string",
"new_password": "string (min 8 chars)"
}
```
- **Response:** `{"message": "Password reset successfully. You can now log in."}`
- **Errors:** 400 invalid/expired/used token
### GET `/api/auth/me`
Get the current authenticated user's profile.
- **Auth:** User
- **Response:** `UserResponse``{id, email, name, role, created_at}`
### PUT `/api/auth/me`
Update current user's profile (name, password).
- **Auth:** User
- **Request body:**
```json
{
"name": "string | null",
"current_password": "string | null (required to change password)",
"new_password": "string | null (min 8 chars)"
}
```
- **Response:** `UserResponse`
### GET `/api/auth/me/settings`
Get user settings stored in Redis (e.g. Nextcloud config).
- **Auth:** User
- **Response:** `{}` or a JSON object of user settings
### PUT `/api/auth/me/settings`
Save user settings to Redis.
- **Auth:** User
- **Request body:** Any JSON object
- **Response:** `{"saved": true}`
---
## Documents (`/api/documents`)
### POST `/api/documents/upload`
Upload a PDF document for processing.
- **Auth:** Moderator
- **Content-Type:** `multipart/form-data`
- **Request:** `file` (PDF, max size from `MAX_UPLOAD_SIZE` setting)
- **Response:** `DocumentResponse``{id, user_id, filename, original_filename, status, total_pages, error_message, uploaded_at}`
- **Notes:** Processing dispatched to Celery (falls back to sync). Status starts as `"processing"`.
### GET `/api/documents/`
List all documents. Moderators see all; regular users see only their own.
- **Auth:** User
- **Response:** `DocumentResponse[]`
### GET `/api/documents/{document_id}`
Get a single document by ID.
- **Auth:** User (own documents only; moderators can access any)
- **Response:** `DocumentResponse`
- **Errors:** 404
### GET `/api/documents/{document_id}/status`
Poll document processing status.
- **Auth:** User (own documents only; moderators can access any)
- **Response:** `{id, status, total_pages, error_message}`
### GET `/api/documents/{document_id}/processing-steps`
Get PDF processing progress steps from Redis.
- **Auth:** User (own documents only; moderators can access any)
- **Response:** `{"steps": [{step object}, ...]}`
- **Notes:** Steps are stored in Redis list `pdf:steps:{document_id}`.
### POST `/api/documents/{document_id}/sections`
Create a named page range section within a document.
- **Auth:** Moderator
- **Request body:**
```json
{
"name": "string",
"start_page": "int (>= 1)",
"end_page": "int (> start_page, <= total_pages)"
}
```
- **Response:** `SectionResponse``{id, document_id, name, start_page, end_page}`
- **Errors:** 400 if document not ready, invalid page range
### DELETE `/api/documents/{document_id}/sections/{section_id}`
Delete a section.
- **Auth:** Moderator
- **Response:** 204 No Content
### DELETE `/api/documents/{document_id}`
Delete a document, its file, and its ChromaDB vector collection.
- **Auth:** Moderator
- **Response:** 204 No Content
---
## Quizzes (`/api/quizzes`)
### POST `/api/quizzes/`
Start async quiz extraction from a document section.
- **Auth:** Moderator
- **Request body:**
```json
{
"section_id": "int",
"title": "string",
"mode": "timed | learning",
"time_limit_minutes": "int | null",
"model_id": "int | null (AIModelConfig.id)",
"question_category_id": "int | null",
"extraction_mode": "string | null (standard, questions_only, two_step, regex, ai_decide, generate)"
}
```
- **Response:** `{"job_id": "uuid", "status": "pending"}`
- **Notes:** Poll `/api/quizzes/job/{job_id}` for progress. Falls back to sync if Celery unavailable.
### GET `/api/quizzes/jobs`
List all recent extraction jobs for the current user (any browser session).
- **Auth:** User
- **Response:**
```json
[
{
"job_id": "uuid",
"title": "string",
"status": "pending | running | completed | failed | cancelled",
"steps_count": "int",
"last_step": "string (truncated to 80 chars)",
"quiz_id": "int | null"
}
]
```
### GET `/api/quizzes/job/{job_id}`
Poll extraction job progress.
- **Auth:** Moderator
- **Response:** `{"job_id", "status", "steps": [...], "quiz_id" (if completed), "error" (if failed)}`
### POST `/api/quizzes/job/{job_id}/cancel`
Cancel a running extraction job.
- **Auth:** User (must own the job)
- **Response:** `{"status": "cancelled"}`
- **Errors:** 403 if not owner, 400 if not running
### GET `/api/quizzes/search`
Hybrid semantic + keyword search across quiz titles and questions.
- **Auth:** User
- **Query params:**
- `q` (string, required, 2-200 chars)
- `mode` (string: `"title"` | `"questions"` | `"all"`, default `"all"`)
- **Response:** Array of search results with `quiz_id`, `quiz_title`, `match_type`, `matching_questions[]`
- **Notes:** Uses pgvector cosine similarity (threshold 0.30) for semantic search, plus ILIKE for keyword matching.
### GET `/api/quizzes/`
List all quizzes. Moderators see all (non-deleted); regular users see only published.
- **Auth:** User
- **Response:** `QuizResponse[]``{id, section_id, user_id, title, questions_count, mode, time_limit_minutes, is_published, created_at}`
### GET `/api/quizzes/{quiz_id}`
Get a quiz for taking. Learning-mode quizzes include answers/explanations.
- **Auth:** User
- **Query params:** `study` (bool, default false) — forces answers/explanations to be included
- **Response:** `QuizDetail` (timed) or `QuizLearningDetail` (learning/study)
- **Notes:** Excludes soft-deleted quizzes.
### PATCH `/api/quizzes/{quiz_id}`
Update quiz metadata (title).
- **Auth:** Moderator
- **Request body:** `{"title": "string"}`
- **Response:** `QuizResponse`
### POST `/api/quizzes/{quiz_id}/shuffle`
Return quiz with shuffled question and option order (does not modify DB).
- **Auth:** User
- **Query params:** `shuffle_options` (bool, default true)
- **Response:** `QuizDetail` with shuffled questions
### GET `/api/quizzes/{quiz_id}/review`
Get quiz with full answers. Requires a completed attempt (or moderator role).
- **Auth:** User (must have completed attempt) or Moderator
- **Response:** `QuizReview`
### GET `/api/quizzes/{quiz_id}/questions`
Get all questions with answers for editing.
- **Auth:** Moderator
- **Response:** Array of `{id, question_text, question_type, options, correct_answer, explanation}`
### PATCH `/api/quizzes/{quiz_id}/questions/{question_id}`
Update a question. Changes are shared across all quizzes referencing this question.
- **Auth:** Moderator
- **Request body:** Any of `{question_text, options, correct_answer, explanation, question_type}`
- **Response:** Updated question object
- **Errors:** 400 if correct_answer does not match one of the options
### DELETE `/api/quizzes/{quiz_id}/questions/{question_id}`
Remove a question from this quiz. If shared with other quizzes, only the link is removed. If exclusive, the question is deleted from the bank.
- **Auth:** Moderator
- **Response:** 204 No Content
### PATCH `/api/quizzes/{quiz_id}/publish`
Show or hide a quiz for regular users.
- **Auth:** Moderator
- **Query params:** `published` (bool, default true)
- **Response:** `{"quiz_id": int, "is_published": int}`
### GET `/api/quizzes/trash`
List soft-deleted quizzes.
- **Auth:** Moderator
- **Response:** `QuizResponse[]`
### DELETE `/api/quizzes/{quiz_id}`
Soft-delete a quiz (move to trash).
- **Auth:** Moderator
- **Response:** 204 No Content
### PATCH `/api/quizzes/{quiz_id}/restore`
Restore a soft-deleted quiz from trash.
- **Auth:** Moderator
- **Response:** `QuizResponse`
### DELETE `/api/quizzes/{quiz_id}/permanent`
Permanently delete a quiz that is already in trash. Exclusive questions are orphaned to the bank.
- **Auth:** Moderator
- **Response:** 204 No Content
- **Errors:** 400 if quiz is not in trash
---
## Questions (`/api/questions`)
### GET `/api/questions/bank`
List all questions across all quizzes with filtering, search, and pagination.
- **Auth:** User
- **Query params:**
- `q` (string, optional) — keyword/semantic search
- `quiz_id` (int, optional) — filter by source quiz
- `category_id` (int, optional) — filter by question category
- `category_ids` (string, optional) — comma-separated category IDs (OR filter)
- `uncategorized` (bool, default false)
- `favorites_only` (bool, default false)
- `tag_ids` (string, optional) — comma-separated tag IDs (AND filter)
- `search_mode` (string: `"keyword"` | `"semantic"` | `"hybrid"`, default `"hybrid"`)
- `limit` (int, default 50, max 200)
- `offset` (int, default 0)
- **Response:**
```json
{
"total": "int",
"questions": [
{
"id": "int",
"quiz_id": "int",
"quiz_title": "string",
"question_category_id": "int | null",
"question_category_name": "string | null",
"question_text": "string",
"question_type": "string",
"options": ["string"],
"correct_answer": "string",
"explanation": "string",
"image_path": "string | null"
}
]
}
```
- **Notes:** Hybrid search returns semantic matches first (pgvector cosine similarity >= 0.55), then keyword matches.
### GET `/api/questions/bank/ids`
Return just question IDs for all matching questions (for server-side select-all).
- **Auth:** User
- **Query params:** Same filters as `/bank` (except `limit`, `offset`, `search_mode`)
- **Response:** `[int]`
### PATCH `/api/questions/{question_id}/category`
Assign or remove a question category.
- **Auth:** Moderator
- **Query params:** `category_id` (int | null)
- **Response:** `{"question_id": int, "question_category_id": int | null}`
### POST `/api/questions/from-bank`
Create a new quiz by referencing existing bank questions (no copying -- edits propagate).
- **Auth:** Moderator
- **Request body:**
```json
{
"title": "string",
"question_ids": [1, 2, 3],
"mode": "timed | learning",
"time_limit_minutes": "int | null"
}
```
- **Response:** `{"id": int, "title": "string", "questions_count": int}`
### POST `/api/questions/bulk-category`
Assign or remove a category from multiple questions at once.
- **Auth:** Moderator
- **Request body:**
```json
{
"question_ids": [1, 2, 3],
"category_id": "int | null"
}
```
- **Response:** `{"updated": int, "question_category_id": int | null}`
---
## Attempts (`/api/attempts`)
### POST `/api/attempts/start`
Start a new quiz attempt (or resume the most recent incomplete one).
- **Auth:** User
- **Query params:**
- `quiz_id` (int, required)
- `fresh` (bool, default false) — force a new attempt even if one is in progress
- **Response:** `AttemptResponse``{id, quiz_id, score, total_questions, percentage, started_at, completed_at}`
### POST `/api/attempts/{attempt_id}/submit`
Submit answers for a quiz attempt.
- **Auth:** User (must own the attempt)
- **Request body:**
```json
{
"answers": [
{"question_id": "int", "user_answer": "string"}
]
}
```
- **Response:** `AttemptDetail` — includes `answers[]` with full review (question text, options, user answer, correct answer, is_correct, explanation)
- **Notes:** Updates spaced repetition reminder schedule. Clears saved progress from Redis. Unanswered questions are marked incorrect.
### GET `/api/attempts/`
List all attempts for the current user.
- **Auth:** User
- **Query params:** `quiz_id` (int, optional)
- **Response:** `AttemptResponse[]`
### GET `/api/attempts/{attempt_id}`
Get a specific attempt with full answer details.
- **Auth:** User (must own the attempt)
- **Response:** `AttemptDetail`
### GET `/api/attempts/stats/dashboard`
Get dashboard statistics for the current user.
- **Auth:** User
- **Response:**
```json
{
"total_documents": "int",
"total_quizzes": "int (distinct quizzes attempted)",
"total_attempts": "int",
"average_score": "float",
"quiz_stats": [
{
"quiz_id": "int",
"quiz_title": "string",
"attempts_count": "int",
"best_score": "float",
"latest_score": "float",
"average_score": "float"
}
]
}
```
### GET `/api/attempts/history`
Get per-quiz attempt history for the performance line graph.
- **Auth:** User
- **Response:**
```json
[
{
"quiz_id": "int",
"title": "string",
"attempts": [
{"attempt_id": "int", "date": "ISO string", "percentage": "float", "score": "int", "total": "int"}
]
}
]
```
### GET `/api/attempts/in-progress`
Return all incomplete (not yet submitted) attempts for the current user.
- **Auth:** User
- **Response:**
```json
[
{
"attempt_id": "int",
"quiz_id": "int",
"quiz_title": "string",
"total_questions": "int",
"started_at": "ISO string"
}
]
```
### GET `/api/attempts/{attempt_id}/in-progress`
Return the latest incomplete attempt for a specific quiz.
- **Auth:** User
- **Query params:** `quiz_id` (int, required)
- **Response:** `AttemptResponse | null`
### POST `/api/attempts/progress`
Save in-progress quiz answers to Redis (survives logout/browser change).
- **Auth:** User
- **Headers:** `x-quiz-session` (optional, for device session lock)
- **Request body:**
```json
{
"quiz_id": "int",
"attempt_id": "int",
"answers": {"question_id": "answer"},
"current_idx": "int",
"mode": "string",
"voice": "string | null",
"time_left": "int | null",
"started_at": "string | null (ISO timestamp)",
"total_time": "int | null (seconds)"
}
```
- **Response:** `{"saved": true}`
- **Notes:** Progress expires after 7 days. Session lock (30s TTL) prevents concurrent resume on another device.
### GET `/api/attempts/progress`
Retrieve saved progress for a quiz. Auto-submits timed quizzes if timer expired.
- **Auth:** User
- **Headers:** `x-quiz-session` (optional, for device session lock)
- **Query params:** `quiz_id` (int, required)
- **Response:** Saved progress object or `null`
- **Errors:** 409 if another device is actively using this attempt
### DELETE `/api/attempts/progress/{attempt_id}`
Clear saved progress.
- **Auth:** User
- **Response:** 204 No Content
### DELETE `/api/attempts/{attempt_id}`
Delete an attempt, its answers, and associated reminder schedule.
- **Auth:** User (must own the attempt)
- **Response:** 204 No Content
---
## Tags (`/api/tags`)
### GET `/api/tags`
Get all tags grouped by type (subjects, diseases, keywords), with question counts.
- **Auth:** User
- **Response:**
```json
{
"subjects": [{"id": "int", "name": "string", "count": "int"}],
"diseases": [{"id": "int", "name": "string", "count": "int"}],
"keywords": [{"id": "int", "name": "string", "count": "int"}]
}
```
### POST `/api/tags/classify`
Trigger background AI classification of untagged questions.
- **Auth:** Moderator
- **Response:** `{"job_id": "uuid", "status": "started"}`
### GET `/api/tags/status/{job_id}`
Check classification job progress.
- **Auth:** User
- **Response:** `{"job_id": "string", "status": "string", "steps": [...], "error": "string | null"}`
---
## Teach (`/api/teach`)
### GET `/api/teach/models`
Return available teach AI models.
- **Auth:** User
- **Response:** `[{"id": int, "name": "string", "model_id": "string", "is_default": bool}]`
### POST `/api/teach/chat`
Send a message to the AI tutor with full question context.
- **Auth:** User
- **Rate limit:** 30 messages per user per 10 minutes (admins and unthrottled users exempt)
- **Request body:**
```json
{
"question_id": "int",
"messages": [
{"role": "user | assistant", "content": "string"}
],
"model_id": "int | null (AIModelConfig.id)"
}
```
- **Response:** `{"reply": "string (markdown)", "suggestions": ["string", "string", "string"]}`
- **Notes:** System prompt includes the question, correct answer, explanation, and up to 4 semantically similar questions for broader context. Follow-up suggestions are parsed from lines starting with `> `.
- **Errors:** 503 if no teach model configured, 502 if AI temporarily unavailable
---
## TTS (`/api/tts`)
### GET `/api/tts/voices`
Return available TTS voices. Excludes Polly voices if Polly is disabled.
- **Auth:** User
- **Response:** `[{"id": "string (model_id)", "name": "string", "is_default": bool}]`
### POST `/api/tts/speak`
Convert text to speech audio.
- **Auth:** User
- **Rate limit:** 60 requests per user per hour (admins and unthrottled users exempt)
- **Request body:**
```json
{
"text": "string (max 2000 chars)",
"voice": "string | null (model_id override)"
}
```
- **Response:** Binary `audio/mpeg` data
- **Notes:** Supports LiteLLM-routed local TTS voices, e.g. `local-kokoro-tts:am_adam`. The prefix before `:` is the LiteLLM model route; the suffix is the speaker voice sent to that route.
---
## Favorites (`/api/favorites`)
### POST `/api/favorites`
Add a question to the current user's favorites.
- **Auth:** User
- **Request body:** `{"question_id": "int"}`
- **Response (201):** `FavoriteResponse``{id, user_id, question_id, created_at}`
- **Notes:** Idempotent -- returns existing favorite if already favorited.
### DELETE `/api/favorites/{question_id}`
Remove a question from favorites.
- **Auth:** User
- **Response:** 204 No Content
### GET `/api/favorites`
Get list of favorited question IDs for the current user.
- **Auth:** User
- **Response:** `[int]`
---
## Categories (`/api/categories`)
Quiz-level categories for organizing quizzes.
### GET `/api/categories/`
List all quiz categories with quiz counts.
- **Auth:** User
- **Response:** `[{"id": int, "name": "string", "quiz_count": int}]`
### POST `/api/categories/`
Create a new quiz category.
- **Auth:** Moderator
- **Request body:** `{"name": "string"}`
- **Response:** `CategoryResponse`
### DELETE `/api/categories/{category_id}`
Delete a quiz category. Unassigns all quizzes in the category.
- **Auth:** Moderator
- **Response:** 204 No Content
### PATCH `/api/categories/quizzes/{quiz_id}`
Assign or remove a category from a quiz.
- **Auth:** Moderator
- **Query params:** `category_id` (int | null)
- **Response:** `{"quiz_id": int, "category_id": int | null}`
---
## Question Categories (`/api/question-categories`)
Question-level categories for organizing the question bank.
### GET `/api/question-categories/`
List all question categories with question counts.
- **Auth:** User
- **Response:** `[{"id": int, "name": "string", "description": "string | null", "question_count": int}]`
### POST `/api/question-categories/`
Create a new question category.
- **Auth:** Moderator
- **Request body:** `{"name": "string", "description": "string | null"}`
- **Response:** `QCatResponse`
### PATCH `/api/question-categories/{cat_id}`
Update a question category.
- **Auth:** Moderator
- **Request body:** `{"name": "string", "description": "string | null"}`
- **Response:** `QCatResponse`
### DELETE `/api/question-categories/{cat_id}`
Delete a question category. Optionally move its questions to another category.
- **Auth:** Moderator
- **Query params:** `move_to` (int | null) -- target category ID, or null to uncategorize
- **Response:** 204 No Content
### POST `/api/question-categories/{cat_id}/create-quiz`
Create a new quiz from all questions in a question category.
- **Auth:** Moderator
- **Query params:**
- `title` (string, required)
- `mode` (string: `"timed"` | `"learning"`, default `"timed"`)
- `time_limit_minutes` (int | null)
- **Response:** `{"id": int, "title": "string", "questions_count": int}`
---
## Contact (`/api/contact`)
### POST `/api/contact`
Submit a contact form (public, no auth).
- **Auth:** None
- **Request body:**
```json
{
"name": "string (max 120)",
"email": "valid email",
"type": "question | moderator",
"message": "string (max 2000)",
"turnstile_token": "string | null"
}
```
- **Response:** `{"success": true}`
- **Notes:** Stores submission in DB and emails admin. Turnstile verification if configured.
### GET `/api/contact/submissions`
List all contact submissions.
- **Auth:** Admin (enforced in endpoint body, not decorator)
- **Response:** `[{id, name, email, type, message, read, created_at}]`
### PUT `/api/contact/submissions/{submission_id}/read`
Mark a contact submission as read.
- **Auth:** Admin (enforced in endpoint body, not decorator)
- **Response:** `{"success": true}`
---
## Admin (`/api/admin`)
### User Management
#### GET `/api/admin/users`
List all users.
- **Auth:** Admin
- **Response:** `UserResponse[]`
#### POST `/api/admin/users`
Create a user directly (email auto-verified).
- **Auth:** Admin
- **Request body:** `{"email": "string", "password": "string", "name": "string"}`
- **Response:** `UserResponse`
#### PUT `/api/admin/users/{user_id}/role`
Change a user's role.
- **Auth:** Admin
- **Request body:** `{"role": "admin | moderator | user"}`
- **Response:** `UserResponse`
- **Errors:** 400 cannot change own role
#### PUT `/api/admin/users/{user_id}/unthrottle`
Set or clear the unthrottle flag (exempt from AI/TTS rate limits).
- **Auth:** Admin
- **Request body:** `{"unthrottled": true | false}`
- **Response:** `UserResponse`
#### DELETE `/api/admin/users/{user_id}`
Delete a user and all their data.
- **Auth:** Admin
- **Response:** 204 No Content
- **Errors:** 400 cannot delete yourself
### AI Model Configuration
#### GET `/api/admin/models/available`
List active models for a task (for users when creating/taking quizzes).
- **Auth:** User
- **Query params:** `task` (string, default `"extraction"`)
- **Response:** `[{"id": int | null, "name": "string", "model_id": "string", "is_default": bool}]`
- **Notes:** Falls back to env `LITELLM_MODEL` if nothing configured.
#### GET `/api/admin/models`
List all AI model configurations.
- **Auth:** Admin
- **Response:** `AIModelConfigResponse[]` -- `{id, name, model_id, task, is_active, is_default, api_key (masked)}`
#### POST `/api/admin/models`
Create an AI model configuration.
- **Auth:** Admin
- **Request body:**
```json
{
"name": "string",
"model_id": "string",
"task": "extraction | tts | teach | keyword",
"is_active": "bool",
"is_default": "bool",
"api_key": "string | null"
}
```
- **Response:** `AIModelConfigResponse`
- **Notes:** Auto-sets as default if first model for that task. Unique constraint on (model_id, task).
#### PUT `/api/admin/models/{model_id}`
Update an AI model configuration.
- **Auth:** Admin
- **Request body:** Partial `AIModelConfigUpdate`
- **Response:** `AIModelConfigResponse`
#### DELETE `/api/admin/models/{model_id}`
Delete an AI model configuration.
- **Auth:** Admin
- **Response:** 204 No Content
#### POST `/api/admin/models/{model_id}/test`
Send a test completion to verify an LLM model is reachable.
- **Auth:** Admin
- **Response:** `{"message": "model_id -> 'OK'"}`
- **Errors:** 400 for TTS models (use Preview button instead), 502 if unreachable
#### POST `/api/admin/litellm/models`
Query available models from LiteLLM proxy or OpenAI-compatible API.
- **Auth:** Admin
- **Request body:** `{"api_key": "string | null", "api_base": "string | null"}`
- **Response:** `{"models": ["string"], "source": "string"}`
### TTS Voice Search
#### POST `/api/admin/tts/voices`
Discover available TTS voices from a provider.
- **Auth:** Admin
- **Request body:**
```json
{
"provider": "elevenlabs | polly | openai",
"api_key": "string | null",
"region": "string | null"
}
```
- **Response:** `{"voices": [{"model_id": "string", "name": "string", "labels": {}}]}`
- **Notes:** OpenAI returns hardcoded list of tts-1 and tts-1-hd voices.
### System Settings
#### GET `/api/admin/settings`
Get system settings (registration enabled, embedding model, Polly enabled).
- **Auth:** Admin
- **Response:** `{"registration_enabled": bool, "embedding_model": "string"}`
#### PUT `/api/admin/settings`
Update system settings.
- **Auth:** Admin
- **Request body:** `{"registration_enabled": bool, "embedding_model": "string"}`
- **Response:** `{"success": true, "message": "Settings updated"}`
### Embedding Management
#### POST `/api/admin/embedding/test`
Test the currently configured embedding model.
- **Auth:** Admin
- **Response:** `{"model": "string", "dimensions": int, "status": "ok"}`
#### POST `/api/admin/embedding/regenerate`
Queue a Celery task to regenerate all question embeddings.
- **Auth:** Admin
- **Response:** `{"job_id": "uuid", "message": "Regeneration started..."}`
---
## Nextcloud (`/api/nextcloud`)
WebDAV proxy for importing PDFs from Nextcloud (avoids browser CORS issues).
### POST `/api/nextcloud/test`
Test Nextcloud credentials.
- **Auth:** Moderator
- **Request body:** `{"server": "string", "username": "string", "password": "string", "path": "/"}`
- **Response:** `{"ok": true, "message": "Connected successfully"}`
### POST `/api/nextcloud/files`
List PDFs and folders at a given Nextcloud path.
- **Auth:** Moderator
- **Request body:** `{"server": "string", "username": "string", "password": "string", "path": "/"}`
- **Response:** `{"path": "string", "items": [{"name", "path", "type": "dir | pdf", "size"}]}`
### POST `/api/nextcloud/download`
Download a PDF from Nextcloud and stream it back.
- **Auth:** Moderator
- **Request body:** `{"server": "string", "username": "string", "password": "string", "path": "/some/file.pdf"}`
- **Response:** Streamed `application/pdf` binary
- **Errors:** 400 if not a PDF file