# API Reference [中文](../zh-CN/API.md) | [English](./API.md) This document describes the HTTP API provided by `server.py`. All interfaces return JSON. Unless otherwise specified, the request body also uses JSON. ## General convention Base address: ```text http://localhost:8092 ``` Request header: ```http Content-Type: application/json Cookie: todo_session= X-CSRF-Token: # required only for mutating requests ``` When accessing some read interfaces without logging in, read-only empty data will be returned; writing, updating, and deleting interfaces must be logged in. Request body constraints: - The top level of the JSON request body must be an object. - The maximum size of a single JSON request body is 5 MiB; the base64 content of the avatar upload is also included in this limit. - Unmatched `/api/...` paths return a JSON 404 instead of an HTML error page. Generic error response: ```json { "error": "error message" } ``` Some business errors will return additional fields such as `message`, `usedMinutes`, `capacityMinutes`, etc., and the front end will display `message` first. ## Data model ### User ```json { "id": 1, "name": "Zhang San", "nickname": "zhangsan", "role": "student" } ``` ### Task ```json { "id": "task-1710000000000-abcd1234", "userId": 1, "title": "Complete physics homework", "subject": "Physics", "dueAt": "2026-05-18T23:59:00", "priority": "medium", "note": "No. 3 Chapter Exercises", "completed": false, "createdAt": "2026-05-18T13:00:00Z", "updatedAt": "2026-05-18T13:00:00Z" } ``` Description: - `id` Up to 128 characters; if not passed when creating the task, it will be generated by the backend. - `title` Required, maximum 120 characters. - `subject` Required, up to 40 characters; value can come from account template or user-defined text. - `priority` Optional values: `high`, `medium`, `low`. - When `dueAt` is an empty string, it indicates an unscheduled task; when it is not empty, `YYYY-MM-DDTHH:mm:ss` must be used. - `note` Maximum 4000 characters. - `completed` must be a JSON boolean. ### ScheduleItem ```json { "id": "schedule-1710000000000-abcd1234", "userId": 1, "taskId": "task-1710000000000-abcd1234", "date": "2026-05-18", "slotKey": "2026-05-18-dynamic", "slotLabel": "Daily schedule", "slotStart": "00:00", "slotEnd": "23:59", "startTime": "18:05", "endTime": "18:35", "durationMinutes": 30, "note": "Review wrong questions", "completed": false, "createdAt": "2026-05-18T13:00:00Z", "updatedAt": "2026-05-18T13:00:00Z", "task": { "id": "task-1710000000000-abcd1234", "title": "Complete physics homework", "subject": "Physics", "dueAt": "2026-05-18T23:59:00", "priority": "medium" } } ``` ### Slot (compatible with older versions) ```json { "keyBase": "1-18:00", "label": "After dinner", "start": "18:00", "end": "18:40" } ``` Description: - The new version of daily schedule no longer relies on the time grid; `slotKey`, `slotLabel`, `slotStart`, `slotEnd` are only returned as old data compatible fields. - `keyBase` is the time grid stable identifier within the same day. - `start` and `end` use `HH:mm`. ## Authentication ### Start central sign-in ```http GET /auth/login ``` Creates one-time OIDC state, nonce, and PKCE verifier, stores the flow hash in SQLite, and redirects to Accounts. The browser also receives a short-lived `HttpOnly` flow cookie. ### OIDC callback ```http GET /auth/callback?code=&state= ``` The backend exchanges the code and validates the ID Token and UserInfo. If the `auth_sub` has no local member, the central username and display name are copied once into a default `student` member. Profiles remain site-local afterward. Success sets the local session cookie and redirects to `/`. Legacy `POST /api/auth/register`, `POST /api/auth/login`, and `PUT /api/auth/password` return `410 Gone` by default in production. ### Get the current user ```http GET /api/auth/me ``` Response: ```json { "csrfToken": "request-csrf-token", "user": { "id": 1, "name": "Zhang San", "nickname": "zhangsan", "role": "student" } } ``` Status code: - `200 OK`: Cookie session is valid. - `401 Unauthorized`: Not signed in or the session expired. ### Log out ```http POST /api/auth/logout ``` Response: ```json { "ok": true } ``` This deletes only the TodoList-local session; it does not sign out of Accounts or other sites. ### Back-Channel Logout ```http POST /auth/backchannel-logout Content-Type: application/x-www-form-urlencoded logout_token= ``` Accounts calls this endpoint. After validating the signature and claims, TodoList deletes sessions by central `sid` or `sub`; a repeated `jti` has no repeated side effect. ## ManageBac backend synchronization All endpoints require the local cookie session; mutating requests also require the CSRF token. Responses are not cacheable. ManageBac credentials may be submitted only over HTTPS, except for local `localhost` development. ### Read sign-in state ```http GET /api/managebac/session ``` Returns whether a decryptable cookie is stored, the connection time, and the last verification time. This endpoint does not contact ManageBac and therefore does not prove that the cookie has not expired. ### Sign in and get a preview ```http POST /api/managebac/session Content-Type: application/json { "account": "student@example.com", "password": "ManageBac password" } ``` The backend signs in once, persists only the AES-GCM-encrypted cookie jar, and immediately returns parsed `tasks` and `meta`. The account and password are not written to the database or operation log. Possible statuses: - `200 OK`: Sign-in succeeded and the task preview is returned. - `401 Unauthorized`: ManageBac rejected the credentials. - `426 Upgrade Required`: A production request did not use HTTPS. - `429 Too Many Requests`: Too many recent failed sign-ins. - `502 Bad Gateway`: ManageBac is unavailable or its page protocol is not recognized. - `503 Service Unavailable`: The cookie encryption key is missing or invalid. ### Get a preview with the saved cookie ```http POST /api/managebac/tasks/preview ``` On success, returns `tasks` and `meta` and persists cookie rotation from ManageBac. An expired cookie returns: ```json { "error": "managebac_reauth_required", "message": "ManageBac sign-in has expired.", "requiresLogin": true } ``` ### Clear the sign-in ```http DELETE /api/managebac/session ``` Deletes the ManageBac cookie stored for the current site user. ## Legacy ManageBac Helper installation package download ### Get the installation package download ```http GET /api/managebac-helper/installer Cookie: todo_session= ``` Behavior: - Login required. - When configuring OSS, return a short-term pre-signed download URL. - When OSS is not configured, return the local installation package file stream. - Each time you successfully generate an OSS URL or send a local installation package, it will be counted in the download statistics. OSS response: ```json { "ok": true, "source": "oss", "url": "https://...", "expiresSeconds": 600, "limit": { "windowHours": 24, "linkLimit": 5, "source": "global", "hasOverride": false }, "usage": { "linkCount": 1 } } ``` Status code: - `200 OK`: Returns the OSS URL or local file stream. - `401 Unauthorized`: Not logged in. - `429 Too Many Requests`: The number of download link generation times reaches the current user limit. ### Administrator views download statistics ```http GET /api/admin/installer-downloads/summary?view=7d&page=1&pageSize=10 ``` `view` Optional: `6h`, `1d`, `7d`, `30d`. ### Administrator updates global download restrictions ```http PUT /api/admin/installer-downloads/global-limit ``` Request: ```json { "windowHours": 24, "linkLimit": 5 } ``` ### Administrator updates single user download limit ```http PUT /api/admin/users/{userId}/installer-download-limit ``` Request: ```json { "windowHours": 24, "linkLimit": 10 } ``` ### Administrator clears single-user download restrictions ```http DELETE /api/admin/users/{userId}/installer-download-limit ``` ### Administrator clears all single-user download restrictions ```http POST /api/admin/installer-downloads/clear-user-limits ``` ## Task interface ### Get task list ```http GET /api/tasks ``` Not logged in response: ```json { "tasks": [], "readOnly": true } ``` Logged in response: ```json { "tasks": [], "readOnly": false, "user": { "id": 1, "name": "Zhang San", "nickname": "zhangsan", "role": "student" } } ``` ### Create task ```http POST /api/tasks ``` Request: ```json { "id": "task-1710000000000-abcd1234", "title": "Complete physics homework", "subject": "Physics", "dueAt": "2026-05-18T23:59:00", "priority": "medium", "note": "No. 3 Chapter Exercises", "completed": false, "createdAt": "2026-05-18T13:00:00Z" } ``` Response: ```json { "ok": true, "task": {} } ``` Status code: - `201 Created`: Created successfully. - `400 Bad Request`: The title or subject is missing, a field is too long, `dueAt` has an invalid format, `priority` / `pool` is invalid, or `completed` is not a Boolean value. - `401 Unauthorized`: Not logged in. - `409 Conflict`: Task id already exists. ### Update task ```http PUT /api/tasks/{id} ``` The request body is the same as the creation task. `{id}` in the path is the task id to be updated. Response: ```json { "ok": true, "task": {} } ``` Status code: - `200 OK`: Update successful. - `400 Bad Request`: The title or subject is missing, a field is too long, `dueAt` has an invalid format, `priority` / `pool` is invalid, or `completed` is not a Boolean value. - `401 Unauthorized`: Not logged in. - `404 Not Found`: The task does not exist or does not belong to the current user. ### Delete task ```http DELETE /api/tasks/{id} ``` Response: ```json { "ok": true } ``` Note: When deleting a single task, the daily schedule corresponding to the task will be deleted simultaneously. ## Daily scheduling interface ### Get daily schedule ```http GET /api/schedule-items ``` Response: ```json { "items": [], "readOnly": false } ``` Returns when not logged in: ```json { "items": [], "readOnly": true } ``` ### Create daily schedule ```http POST /api/schedule-items ``` Request: ```json { "taskId": "task-1710000000000-abcd1234", "date": "2026-05-18", "startTime": "18:05", "durationMinutes": 30, "note": "Review wrong questions", "completed": false } ``` Response: ```json { "ok": true, "id": "schedule-1710000000000-abcd1234" } ``` Status code: - `201 Created`: Created successfully. - `400 Bad Request`: The task does not exist, the date or start time is missing, the duration is illegal, or the task is scheduled to span to the next day. - `401 Unauthorized`: Not logged in. Daily schedule no longer relies on time period templates. End time is automatically calculated by `startTime + durationMinutes`; schedules for the same date allow overlapping times. When querying the schedule, each instance returns: ```json { "startTime": "18:05", "endTime": "18:35", "hasConflict": true, "conflictIds": ["schedule-overlapping-id"] } ``` Adjacent but non-overlapping intervals are not marked as conflicting; completed arrangements no longer participate in conflict calculations. ### Update daily schedule ```http PUT /api/schedule-items/{id} ``` The current implementation only updates the following fields: ```json { "startTime": "18:10", "durationMinutes": 20, "note": "Change to multiple choice questions", "completed": true } ``` Response: ```json { "ok": true, "id": "schedule-1710000000000-abcd1234" } ``` Status code: - `200 OK`: Update successful. - `400 Bad Request`: The start time or duration is illegal, or the schedule spans to the next day. - `401 Unauthorized`: Not logged in. - `404 Not Found`: The schedule does not exist or does not belong to the current user. ### Delete daily schedule ```http DELETE /api/schedule-items/{id} ``` Response: ```json { "ok": true } ``` Note: Deleting a regular daily schedule does not delete its source task. Deleting a habit instance permanently skips that date for the habit without affecting the habit definition or its other dates; subsequent synchronization will not recreate it. ## Time grid configuration interface ### Get time grid configuration ```http GET /api/schedule-config ``` Response: ```json { "defaultWeekSlots": { "0": [], "1": [] }, "templateVersions": [ { "id": 1, "effectiveFrom": "2026-05-18", "slots": { "0": [], "1": [] }, "createdAt": "2026-05-18T13:00:00Z", "updatedAt": "2026-05-18T13:00:00Z" } ], "dayOverrides": { "2026-05-18": [] }, "readOnly": false } ``` Description: - `defaultWeekSlots` is the system’s default weekly template. - `templateVersions` is the one-week template version saved by the user. Press `effectiveFrom` to take effect. - `dayOverrides` is a custom time grid for a single day, with a higher priority than the weekly template. ### Save template for one week ```http PUT /api/schedule-template ``` Request: ```json { "effectiveFrom": "2026-05-18", "slots": { "0": [ { "keyBase": "0-09:00", "label": "morning", "start": "09:00", "end": "10:00" } ], "1": [], "2": [], "3": [], "4": [], "5": [], "6": [] } } ``` Response: ```json { "ok": true } ``` Note: The template takes effect from `effectiveFrom` for future dates that are not individually customized. If the modification will destroy the time period of the existing arrangement, the interface returns `409 Conflict`. ### Save single day time grid ```http PUT /api/schedule-day-slots/{date} ``` Request: ```json { "slots": [ { "keyBase": "0-13:00", "label": "Lunch break", "start": "13:00", "end": "13:45" } ] } ``` Response: ```json { "ok": true } ``` Note: Only affects `{date}` in the path, such as `2026-05-18`. ### Reset the single day time grid ```http DELETE /api/schedule-day-slots/{date} ``` Response: ```json { "ok": true } ``` Description: Delete the single-day override for this date and make it use the week template again. ### Reset all time grid configurations ```http DELETE /api/schedule-config ``` Response: ```json { "ok": true } ``` Description: Delete the current user's weekly template version and all single-day coverage, and restore the system default template. If restoring the default template will destroy the scheduled time period, the interface returns `409 Conflict`. ## Habits API ### Get the habit list ```http GET /api/habits ``` Not logged in response: ```json { "habits": [], "readOnly": true } ``` Logged in response: ```json { "habits": [], "readOnly": false } ``` ### Create a habit ```http POST /api/habits ``` Request: ```json { "id": "habit-1710000000000-abcd1234", "title": "Memorize words", "subject": "English B", "weekdays": ["1", "3", "5"], "startTime": "18:00", "durationMinutes": 15, "startDate": "2026-05-18", "endDate": "", "priority": "medium", "note": "Review wrong question book", "active": true } ``` Response: ```json { "ok": true, "habit": {} } ``` Status code: - `201 Created`: Created successfully, and a daily schedule that conforms to the date range is generated simultaneously. - `400 Bad Request`: Field is missing, day of the week is illegal, start time or duration is illegal, or habit spans midnight. - `401 Unauthorized`: Not logged in. Get used to not relying on a weekly grid template anymore. The end time is automatically calculated by `startTime + durationMinutes`; it is created normally even if it overlaps with other schedules, and is marked by `hasConflict` and `conflictIds` in the daily schedule query results. ### Update habits ```http PUT /api/habits/{id} ``` Request body and creation convention. Updates will rebuild unfinished custom arrangements for today and the future; historical records are retained. ### Delete habits ```http DELETE /api/habits/{id} ``` Response: ```json { "ok": true } ``` Note: Deleting a habit archives it and deletes all incomplete instances across every date. Completed instances are retained as historical records. ## User feedback interface ### Get my feedback ```http GET /api/feedback ``` Response: ```json { "feedback": [], "feedbackLimitPerUser": 10 } ``` Status code: - `200 OK`: Obtained successfully. - `401 Unauthorized`: Not logged in. ### Submit feedback ```http POST /api/feedback ``` Request: ```json { "content": "Hope to add export function" } ``` Response: ```json { "ok": true, "feedback": {} } ``` Status code: - `201 Created`: Submission successful. - `400 Bad Request`: Content is empty or exceeds 1000 characters. - `401 Unauthorized`: Not logged in. - `409 Conflict`: The number of unanswered feedback reaches the upper limit set by the administrator. ### Delete my feedback ```http DELETE /api/feedback/{id} ``` Response: ```json { "ok": true, "id": 1 } ``` Note: Only the currently logged in user's own feedback can be deleted. ## AI assistant interface The AI assistant only generates instructions to be approved and does not directly write tasks in the `/api/ai/...` interface. The client still calls the common task interface during approval execution, so the back-end verification of the task interface is the final guarantee. The AI ​​context only contains the unfinished `pool=todo` tasks of the current user, and a maximum of 200 tasks are returned to the model; if the actual number exceeds 200, the `taskSelection.truncated` in the response context will prompt the model to ask the user to narrow the range. ### Non-streaming chat ```http POST /api/ai/chat Cookie: todo_session= X-CSRF-Token: Content-Type: application/json ``` Request: ```json { "message": "Help me create a math task, low priority, put it to be scheduled first", "history": [ { "role": "user", "content": "Previous round of user messages" }, { "role": "assistant", "content": "Last round of assistant reply" } ], "locale": "en", "clientNow": "2026-06-20T12:00:00.000Z", "timezone": "Asia/Shanghai" } ``` `locale` accepts `zh-CN` or `en`. When it is `en`, the AI assistant writes user-facing replies in English. The default is `zh-CN`. Response: ```json { "ok": true, "reply": "Reply to the user", "actions": [ { "id": "ai-action-1", "type": "create_task", "summary": "Create a task: Complete the exercise / Mathematics / To be arranged / Low", "task": { "title": "Complete exercise", "subject": "Mathematics", "dueAt": "", "priority": "low", "note": "" } } ], "rejectedActions": [] } ``` Status code: - `200 OK`: AI return has been processed; `actions` still needs client approval before execution. - `400 Bad Request`: The request body is illegal, `message` is missing or exceeds 2000 characters. - `401 Unauthorized`: Not logged in. - `429 Too Many Requests`: Current user AI token limit reached. - `502 Bad Gateway`: DeepSeek request failed. - `503 Service Unavailable`: `DEEPSEEK_API_KEY` is not configured. ### Streaming Chat ```http POST /api/ai/chat-stream Cookie: todo_session= X-CSRF-Token: Content-Type: application/json ``` The request body is the same as `/api/ai/chat`. Respond using Server-Sent Events: ```text event: delta data: {"text":"Let me sort it out first..."} event: done data: {"reply":"Full reply","actions":[],"rejectedActions":[]} ``` Errors are returned as SSE `error` events: ```text event: error data: {"error":"DeepSeek request failed","message":"..."} ``` ### AI action format The server only accepts two types of actions: - `create_task`: can only contain `title`, `subject`, `dueAt`, `priority`, `note`, and is ultimately created as `pool=todo`. - `update_task`: Only `title`, `subject`, `dueAt`, `priority`, `note` of existing tasks in the context can be modified. The server will refuse to delete tasks, mark completed, modify daily schedules, modify habits, write unknown fields, use out-of-context task ids, overlong fields or illegal dates. Rejected actions will appear in `rejectedActions`; the backend will try to get the model to regenerate safe actions with repair hints. ## Administrator AI Token statistics and restrictions The following interfaces require administrator login. ### Get AI usage statistics ```http GET /api/admin/ai-usage/summary?view=7d&page=1&pageSize=10 Cookie: todo_session= ``` `view` Optional: `6h`, `1d`, `7d`, `30d`. The response contains global limits, trend data, effective limits for the current page user, and rolling window usage. ### Update global restrictions ```http PUT /api/admin/ai-usage/global-limit Cookie: todo_session= X-CSRF-Token: Content-Type: application/json ``` Request: ```json { "windowHours": 24, "inputTokenLimit": 200000, "outputTokenLimit": 50000 } ``` ### Update or clear user restrictions ```http PUT /api/admin/users/{userId}/ai-token-limit DELETE /api/admin/users/{userId}/ai-token-limit ``` `PUT` The request body is the same as global restrictions; `DELETE` will cause the user to reuse global restrictions. ### Clear all user overrides ```http POST /api/admin/ai-usage/clear-user-limits ``` Response: ```json { "ok": true, "deleted": 3 } ``` ## Health check ```http GET /api/health ``` Response: ```json { "ok": true } ``` ## Subject Template API Subject templates are per-user presets for the task `subject` field. Tasks still store `subject` as plain text and do not require the value to come from the template. ### Get subject template ```http GET /api/subject-template ``` Response: ```json { "subjects": [ { "name": "Chinese", "preset": true, "enabled": true }, { "name": "Mathematics", "preset": true, "enabled": true }, { "name": "Computer Science", "preset": true, "enabled": true }, { "name": "History", "preset": false, "enabled": true } ], "defaultSubjects": ["Chinese", "Mathematics", "English B", "IELTS", "Physics", "Economics", "Chemistry", "Psychology", "Biology", "Computer Science"], "readOnly": false } ``` Status: - `200 OK`: success. - `401 Unauthorized`: login required. ### Update subject template ```http PUT /api/subject-template ``` Request: ```json { "subjects": [ { "name": "Chinese", "preset": true, "enabled": false }, { "name": "History", "preset": false, "enabled": true } ] } ``` Notes: - Preset subjects are always kept by the server; clients can only enable or disable them. - Custom subjects can be added or removed by including or omitting them from `subjects`. - Subject names are trimmed, case-insensitively deduplicated, and limited to 40 characters. Status: - `200 OK`: saved. - `400 Bad Request`: invalid payload. - `401 Unauthorized`: login required.