# 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 OpenAI TTS (`tts-1:alloy`), ElevenLabs (`elevenlabs/`), Google Cloud TTS (`google/`), and AWS Polly (`polly/`). --- ## 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", "polly_enabled": bool}` #### PUT `/api/admin/settings` Update system settings. - **Auth:** Admin - **Request body:** `{"registration_enabled": bool, "embedding_model": "string", "polly_enabled": bool}` - **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