{"success":true,"data":{"version":"v1","catalog_revision":"0fbec0c5dc819664f8a27518af0dc7b58770379dd51cc665638cbb37f564fc20","deployment_revision":"f73cac1f9c98676f60ee00945dc8b384492c8f8d","base_url":"https://api.ralston.is","apps":["auth","tasks","meals","fitness","notes","budgeting","cpe","wardrobe","calendar","notifications","uploads","economics","hub","health","orka"],"endpoint_count":334,"endpoints":[{"path":"/api/v1/auth/role","method":"GET","description":"Get the current user's role. Returns 'admin' or 'user'. Defaults to 'user' when no explicit role has been assigned.","outputSchema":{"type":"object","properties":{"role":{"type":"string","enum":["admin","user"],"description":"The user's role."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"auth"},{"path":"/api/v1/account","method":"DELETE","description":"Permanently delete the authenticated user's own account. The user is derived from the verified token — a user can only delete themselves. Removing the auth.users row cascades into every app schema, deleting the user's data. This action cannot be undone.","outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean","description":"True when the account was deleted."}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"auth"},{"path":"/api/v1/tasks/projects","method":"GET","description":"List your personal task projects (tasks.projects) that have at least one linked task, with progress (task_count, completed_count) and the next not-done task for each. Archived projects are excluded unless include_archived=true; projects in MCP-excluded contexts are filtered out. Prompt-chain listing has been removed: type=prompt_chain and the former chain-scoping filters (repository_id, organization_id, project_id) answer 410 CHAIN_PROJECTS_REMOVED. Use tasks_projects_read for one project's state fields and document checklist.","inputSchema":{"type":"object","properties":{"type":{"type":"string","enum":["general"],"description":"Optional filter by project type. Only \"general\" is supported; \"prompt_chain\" answers 410 CHAIN_PROJECTS_REMOVED."},"include_archived":{"type":"boolean","description":"Include archived projects in results. Defaults to false."}}},"outputSchema":{"type":"object","properties":{"projects":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The tasks.projects id."},"name":{"type":"string"},"type":{"type":"string"},"has_context":{"type":"boolean","description":"Always false — tasks.projects carries no prompt context."},"is_archived":{"type":"boolean"},"archived_at":{"type":["string","null"]},"task_count":{"type":"number"},"completed_count":{"type":"number"},"next_task":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/projects","method":"POST","description":"Create a new project for organizing related tasks. Use when you need to group tasks under a common goal or theme. Returns the project_id for use with tasks_create and tasks_update.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Project name."},"description":{"type":"string","description":"Project description/context."},"goal":{"type":"string","description":"Project goal."},"color":{"type":"string","description":"Display color."},"icon":{"type":"string","description":"Display icon."}}},"outputSchema":{"type":"object","properties":{"project_id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"goal":{"type":["string","null"]},"url":{"type":"string","description":"Web URL for the created project."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id","method":"GET","description":"Get one of your personal task projects (tasks.projects) with its state fields, its linked tasks and its document checklist. State: current_state (what is going on now), state_updated_at (when current_state was last reported; null if never), ambition (hobby | potential_product | potential_startup | null), pipeline_stage and promotion_trigger. documents lists every type in your document-type catalog: expected ones first (always expected, or listed for the project's ambition), then optional ones, each with authored (a document exists), document_id and updated_at; document content is not included, read it with tasks_projects_documents_read. document_completeness counts the expected types that have a document. Update state with tasks_projects_state. Ids that are not your tasks.projects rows return 404.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"goal":{"type":["string","null"]},"color_slot":{"type":["string","number","null"]},"icon":{"type":["string","null"]},"slug":{"type":["string","null"]},"is_archived":{"type":"boolean"},"archived_at":{"type":["string","null"]},"current_state":{"type":["string","null"]},"state_updated_at":{"type":["string","null"],"description":"When current_state was last reported. Null if it never was."},"ambition":{"type":["string","null"],"enum":["hobby","potential_product","potential_startup",null]},"pipeline_stage":{"type":["string","null"]},"promotion_trigger":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"tasks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"status":{"type":"string"},"priority":{"type":["string","null"]},"due_date":{"type":["string","null"]}}}},"task_count":{"type":"number"},"documents":{"type":"array","description":"Document checklist: expected types first, then optional ones, each in catalog order.","items":{"type":"object","properties":{"type_slug":{"type":"string"},"name":{"type":"string"},"expected":{"type":"boolean"},"authored":{"type":"boolean"},"document_id":{"type":["string","null"]},"updated_at":{"type":["string","null"]}}}},"document_completeness":{"type":"object","properties":{"present":{"type":"number","description":"Expected document types that have a document."},"total":{"type":"number","description":"Expected document types."}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id","method":"PATCH","description":"Update a project's name, description, goal, color, or icon. Only provided fields are changed.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"New project name."},"description":{"type":"string","description":"New description. Pass null to clear."},"goal":{"type":"string","description":"New goal."},"color":{"type":"string","description":"New color."},"icon":{"type":"string","description":"New icon."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"goal":{"type":["string","null"]},"color_slot":{"type":["string","number","null"]},"icon":{"type":["string","null"]},"is_archived":{"type":"boolean"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id/summary","method":"GET","description":"Get a summary of one of your personal task projects (tasks.projects) with task statistics and active task list. Returns total tasks, completed count, completion percentage, overdue count, and a list of active (non-completed, non-archived) tasks. Respects MCP context exclusions. Ids that are not your tasks.projects rows return 404.","outputSchema":{"type":"object","properties":{"project":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"goal":{"type":"string"},"is_archived":{"type":"boolean"},"created_at":{"type":"string"}}},"stats":{"type":"object","properties":{"total":{"type":"number"},"completed":{"type":"number"},"completion_pct":{"type":"number"},"overdue":{"type":"number"}}},"active_tasks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"priority":{"type":"string"},"due_date":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/projects/document-types","method":"GET","description":"List your catalog of project document types (tasks.document_types), in catalog order. Each type has a slug (used by tasks_projects_documents_read and tasks_projects_documents_update), a name, a starter template (markdown a new document can begin from) and the rules for which projects expect it: always_expected, or expected_for_ambitions (the project's ambition is in the list).","outputSchema":{"type":"object","properties":{"document_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"template":{"type":"string"},"always_expected":{"type":"boolean"},"expected_for_ambitions":{"type":"array","items":{"type":"string"}},"sort_order":{"type":"number"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id/documents/:type_slug","method":"GET","description":"Read one document of one of your task projects, by document type slug (for example 'baseline'). The project's types and which have documents are in tasks_projects_read's documents checklist; the catalog is tasks_projects_document_types. Errors are 404 with distinct messages, checked in this order: Project not found (not your tasks.projects row), Document type not found (slug not in your catalog), Document not authored (no document of that type yet; write one with tasks_projects_documents_update).","outputSchema":{"type":"object","properties":{"document":{"type":"object","properties":{"id":{"type":"string"},"type_slug":{"type":"string"},"name":{"type":"string"},"content":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id/documents/:type_slug","method":"PATCH","description":"Write one document of one of your task projects, by document type slug: creates it if missing, otherwise replaces its content. content fully replaces what was there; an empty string clears it without deleting the document. The slug must be in your catalog (tasks_projects_document_types); 404 Project not found or Document type not found otherwise.","inputSchema":{"type":"object","required":["content"],"properties":{"content":{"type":"string","description":"The document's full new content (markdown). Empty string clears it."}}},"outputSchema":{"type":"object","properties":{"document":{"type":"object","properties":{"id":{"type":"string"},"type_slug":{"type":"string"},"name":{"type":"string"},"content":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id/state","method":"GET","description":"Read the state of one of your task projects: current_state, state_updated_at (when current_state was last reported; null if never, which counts as stalest), ambition, pipeline_stage and promotion_trigger. 404 if it is not your tasks.projects row.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"current_state":{"type":["string","null"]},"state_updated_at":{"type":["string","null"],"description":"When current_state was last reported. Null if it never was."},"ambition":{"type":["string","null"],"enum":["hobby","potential_product","potential_startup",null]},"pipeline_stage":{"type":["string","null"]},"promotion_trigger":{"type":["string","null"]},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id/state","method":"PATCH","description":"Update the state of one of your task projects. Only provided fields change; at least one is required. Use current_state to record what is going on with the project (for example 'blocked on X'): supplying it always sets state_updated_at to now, which is what makes a project fresh again; an empty string clears it. ambition (hobby | potential_product | potential_startup, or null to clear), pipeline_stage (where a potential_startup sits in the pipeline) and promotion_trigger (the condition that would graduate the project) never move state_updated_at. 404 if it is not your tasks.projects row.","inputSchema":{"type":"object","properties":{"current_state":{"type":"string","description":"What is going on with the project now. Sets state_updated_at to now. Empty string clears it."},"ambition":{"type":["string","null"],"enum":["hobby","potential_product","potential_startup",null],"description":"Ambition, or null to clear."},"pipeline_stage":{"type":["string","null"],"description":"Startup pipeline stage, or null to clear."},"promotion_trigger":{"type":["string","null"],"description":"Condition that would graduate the project, or null to clear."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"current_state":{"type":["string","null"]},"state_updated_at":{"type":["string","null"],"description":"When current_state was last reported. Null if it never was."},"ambition":{"type":["string","null"],"enum":["hobby","potential_product","potential_startup",null]},"pipeline_stage":{"type":["string","null"]},"promotion_trigger":{"type":["string","null"]},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id/archive","method":"POST","description":"Archive a project. Archived projects are hidden from default views but preserved for history. Use to clean up completed or inactive projects.","outputSchema":{"type":"object","properties":{"project_id":{"type":"string"},"name":{"type":"string"},"is_archived":{"type":"boolean","enum":[true]},"archived_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/projects/:project_id/archive","method":"DELETE","description":"Unarchive a project. Restores an archived project to active visibility in default views.","outputSchema":{"type":"object","properties":{"project_id":{"type":"string"},"name":{"type":"string"},"is_archived":{"type":"boolean","enum":[false]},"archived_at":{"type":"null"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/:task_id/complete","method":"PATCH","description":"Mark a task completed, or report it failed (status='failed' returns it to the backlog). Returns 400 if the task is already completed and 404 for tasks that are missing, not yours, or in MCP-excluded contexts.","inputSchema":{"type":"object","properties":{"execution_notes":{"type":"string","description":"Optional summary of what was done."},"status":{"type":"string","enum":["completed","failed"],"description":"Task outcome. Defaults to 'completed'. 'failed' returns the task to the backlog."},"failure_reason":{"type":"string","description":"Why the task failed. Echoed back in the response."}}},"outputSchema":{"type":"object","properties":{"task_id":{"type":"string"},"status":{"type":"string","enum":["completed","backlog"]},"execution_notes":{"type":["string","null"]},"failure_reason":{"type":["string","null"]},"message":{"type":"string"},"unblocked_tasks":{"type":"array","description":"Always empty; kept for response compatibility.","items":{"type":"object"}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/:task_id/verification","method":"PATCH","description":"Check or uncheck one item of a task's advisory verification checklist by item id. Works on any task you own; returns 404 for unknown tasks/items or MCP-excluded tasks and 409 CONFLICT if the task changed concurrently (re-read and retry). Returns progress as \"n/m\". The checklist never blocks completion.","inputSchema":{"type":"object","required":["item_id","checked"],"properties":{"item_id":{"type":"string","description":"The ID of the verification item (e.g. \"v1\", \"v2\")."},"checked":{"type":"boolean","description":"Whether the item should be checked or unchecked."}}},"outputSchema":{"type":"object","properties":{"task_id":{"type":"string"},"item_id":{"type":"string"},"checked":{"type":"boolean"},"all_verified":{"type":"boolean"},"verification_progress":{"type":"string","description":"Progress string like \"3/5\"."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/create","method":"POST","description":"Create a new standalone task in the backlog. Use when you need to track a single action item outside of a prompt chain, such as a bug fix, follow-up, or ad-hoc task. Supports recurring tasks — set recurrence_frequency to create a task that auto-generates the next instance when completed. Optional verification_items adds an advisory checklist (it never blocks completion); recurring instances inherit the items unchecked. Rejects creation if project_id belongs to an MCP-excluded context.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Task title."},"description":{"type":"string","description":"Optional details."},"project_id":{"type":"string","description":"Optional project to link the task to."},"priority":{"type":"string","enum":["low","medium","high","urgent"],"description":"Task priority. Defaults to medium."},"due_date":{"type":"string","format":"date","description":"Optional due date (YYYY-MM-DD). For recurring tasks, defaults to today if not set."},"status":{"type":"string","enum":["inbox","backlog","ready","in_progress"],"description":"Initial status. Defaults to backlog."},"recurrence_frequency":{"type":"string","enum":["daily","weekdays","weekly","monthly"],"description":"Set to make this a recurring task. When completed, the next instance is auto-generated."},"recurrence_days":{"type":"array","items":{"type":"number"},"description":"Days of week for 'weekdays' frequency (1=Mon, 7=Sun). Defaults to Mon-Fri."},"recurrence_end_date":{"type":"string","format":"date","description":"Optional end date for recurrence (YYYY-MM-DD). After this date, no more instances are generated."},"effort":{"type":"string","enum":["trivial","moderate","deep"],"description":"Cognitive effort level: trivial (autopilot), moderate (engaged), deep (sustained focus). Defaults to null (treated as moderate)."},"estimated_minutes":{"type":"number","description":"Estimated duration in minutes. Optional."},"assigned_to":{"type":"string","description":"UUID of a family member to assign this task to (from public.family_members)."},"idempotency_key":{"type":"string","maxLength":256,"description":"Optional key for safe retries (1-256 visible ASCII chars; also accepted as the Idempotency-Key header). Reusing a key with the same body returns the original task (idempotent_replay: true) without creating another; reusing it with a different body returns 409."},"verification_items":{"type":"array","maxItems":50,"description":"Optional advisory checklist (max 50 items) on any task. Entries are label strings or {id?, label, checked?} objects. Never blocks completion; the next instance of a recurring task gets the same items, all unchecked.","items":{"oneOf":[{"type":"string","description":"Item label; starts unchecked."},{"type":"object","required":["label"],"properties":{"id":{"type":"string","description":"Optional stable id (max 64 chars); generated as v1, v2, ... when omitted."},"label":{"type":"string","description":"Item text (max 500 chars)."},"checked":{"type":"boolean","description":"Defaults to false."}}}]}}}},"outputSchema":{"type":"object","properties":{"task_id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"verification_items":{"type":"array","items":{"type":"object","required":["id","label","checked"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"checked":{"type":"boolean"}}}},"url":{"type":"string"},"idempotency_key":{"type":"string"},"idempotent_replay":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/today","method":"GET","description":"Get today's planned and day-anchored tasks. Returns tasks that are scheduled for today (start_date = due_date = today) and any currently in-progress tasks. Tasks in MCP-excluded contexts are filtered out. Each task includes time-slot fields (starts_at, ends_at, time_block) from task_plans if scheduled to a specific time. Use to see the daily work plan.","outputSchema":{"type":"object","properties":{"date":{"type":"string","description":"The day returned, YYYY-MM-DD."},"tasks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"priority":{"type":["string","null"]},"effort":{"type":["string","null"]},"estimated_minutes":{"type":["number","null"]},"project":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"contexts":{"type":"array","items":{"type":"string"}},"is_recurring":{"type":"boolean"},"verification":{"type":["object","null"],"description":"Checklist progress (checked of total); null when the task has no checklist.","properties":{"checked":{"type":"number"},"total":{"type":"number"}}},"starts_at":{"type":["string","null"]},"ends_at":{"type":["string","null"]},"time_block":{"type":["string","null"]}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/contexts","method":"GET","description":"List all contexts (GTD-style: Work, Home, Errands, etc.) for the authenticated user. Use to discover available contexts before setting MCP exclusions.","outputSchema":{"type":"object","properties":{"contexts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"icon":{"type":["string","null"]},"color":{"type":["string","null"]},"is_active":{"type":"boolean"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/settings/mcp-exclusions","method":"GET","description":"View which contexts are currently excluded from MCP task tool results. Tasks and projects belonging to excluded contexts are hidden from all MCP task queries.","outputSchema":{"type":"object","properties":{"excluded_context_ids":{"type":"array","items":{"type":"string"},"description":"Array of excluded context UUIDs."},"excluded_contexts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"description":"Excluded contexts with resolved names."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/settings/mcp-exclusions","method":"POST","description":"Update the list of contexts excluded from MCP task tool results. Pass the full list of context IDs to exclude — this replaces any previous exclusions. Use tasks_contexts to list available contexts first.","inputSchema":{"type":"object","required":["context_ids"],"properties":{"context_ids":{"type":"array","items":{"type":"string"},"description":"Array of context UUIDs to exclude. Pass an empty array to clear all exclusions."}}},"outputSchema":{"type":"object","properties":{"excluded_context_ids":{"type":"array","items":{"type":"string"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/list","method":"GET","description":"List and filter tasks with flexible query parameters. Supports filtering by status, priority, project, context, due date, and text search. Results are paginated and exclude tasks in MCP-excluded contexts. Each task carries verification ({checked, total}, or null) for its checklist progress. Examples: 'Show all high-priority backlog tasks' → status=backlog&priority=high. 'What tasks are overdue?' → is_overdue=true. 'List tasks in project X' → project_id=<uuid>.","inputSchema":{"type":"object","properties":{"status":{"type":"string","description":"Filter by status. Single value or comma-separated: 'backlog', 'planned', 'in_progress', 'completed', 'blocked', 'archived'. Example: 'backlog,in_progress'."},"priority":{"type":"string","description":"Filter by priority. Single value or comma-separated: 'low', 'medium', 'high', 'urgent'. Example: 'high,urgent'."},"project_id":{"type":"string","description":"Filter to tasks in a specific project (UUID)."},"context_id":{"type":"string","description":"Filter to tasks in a specific context (UUID)."},"tag_id":{"type":"string","description":"Filter to tasks with a specific tag (UUID)."},"tag":{"type":"string","description":"Filter to tasks with a tag by name (case-insensitive). Alternative to tag_id when you know the tag name but not the UUID."},"due_before":{"type":"string","format":"date","description":"Tasks due on or before this date (YYYY-MM-DD)."},"due_after":{"type":"string","format":"date","description":"Tasks due on or after this date (YYYY-MM-DD)."},"is_overdue":{"type":"string","enum":["true","false"],"description":"If 'true', returns only tasks past their due date that are not completed or archived."},"overdue":{"type":"string","enum":["true","false"],"description":"Alias for is_overdue. If both are passed they must agree."},"search":{"type":"string","description":"Text search on task name and description (case-insensitive)."},"sort":{"type":"string","enum":["created_at","updated_at","due_date","priority","title"],"description":"Sort field. Default: created_at."},"order":{"type":"string","enum":["asc","desc"],"description":"Sort order. Default: desc."},"limit":{"type":"string","description":"Max results to return (1-100, default 50)."},"offset":{"type":"string","description":"Pagination offset (default 0)."}}},"outputSchema":{"type":"object","properties":{"tasks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"priority":{"type":"string"},"effort":{"type":["string","null"],"enum":["trivial","moderate","deep",null],"description":"Cognitive effort level, or null."},"due_date":{"type":["string","null"]},"project":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"contexts":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":["string","null"]}}},"description":"Tags assigned to this task."},"verification":{"type":["object","null"],"properties":{"total":{"type":"number"},"checked":{"type":"number"}}}}}},"total":{"type":"number","description":"Total matching tasks."},"limit":{"type":"number"},"offset":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/tags","method":"GET","description":"List all tags for the authenticated user with task counts. Use to discover available tags for filtering tasks (via tag_id or tag query params on tasks/list).","outputSchema":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":["string","null"]},"task_count":{"type":"number","description":"Number of tasks with this tag."},"created_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/:task_id","method":"GET","description":"Get full details for a single task by ID. Returns all fields including description, dates, project info, context names, subtask count, and the advisory verification checklist with its progress. Returns 404 if the task is in an MCP-excluded context.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string"},"priority":{"type":"string"},"effort":{"type":["string","null"],"enum":["trivial","moderate","deep",null],"description":"Cognitive effort level, or null."},"due_date":{"type":["string","null"]},"start_date":{"type":["string","null"]},"completed_at":{"type":["string","null"]},"estimated_minutes":{"type":["number","null"]},"parent_task_id":{"type":["string","null"]},"source_app":{"type":["string","null"]},"external_ref":{"type":["string","null"]},"project":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"contexts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"subtask_count":{"type":"number"},"verification_items":{"type":"array","items":{"type":"object","required":["id","label","checked"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"checked":{"type":"boolean"}}}},"verification":{"type":["object","null"],"description":"Checklist progress (checked of total); null when the task has no checklist.","properties":{"checked":{"type":"number"},"total":{"type":"number"}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/:task_id","method":"PATCH","description":"Update an existing task. Only provided fields are changed (true PATCH semantics). Can modify name, description, status, priority, effort, estimated_minutes, dates, project, and contexts. Returns 404 for tasks in MCP-excluded contexts. Status values: backlog, planned, in_progress, completed, blocked, archived. Priority values: low, medium, high, urgent. Pass null to clear due_date, start_date, priority, description, or project_id. Pass context_ids as an array to replace all context assignments. Any task can carry an advisory verification checklist: pass verification_items to replace it (null or [] clears it), or verification_items_remove_indices and/or verification_items_add to edit it in place (409 CONFLICT if the task changed concurrently; re-read and retry). The checklist never blocks completion. Former prompt-chain fields (prompt_content, depends_on_task_ids, assigned_agent_slug, parallel_group) return 422 NOT_PROMPT_CHAIN_TASK.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"New task title."},"description":{"type":["string","null"],"description":"New description, or null to clear."},"status":{"type":"string","enum":["backlog","planned","in_progress","completed","blocked","archived"],"description":"New status."},"priority":{"type":["string","null"],"enum":["low","medium","high","urgent",null],"description":"New priority, or null to clear."},"effort":{"type":["string","null"],"enum":["trivial","moderate","deep",null],"description":"Cognitive effort level: trivial (autopilot), moderate (engaged), deep (sustained focus). Null to clear."},"estimated_minutes":{"type":["number","null"],"description":"Estimated duration in minutes, or null to clear."},"due_date":{"type":["string","null"],"description":"Due date (YYYY-MM-DD) or null to clear."},"start_date":{"type":["string","null"],"description":"Start date (YYYY-MM-DD) or null to clear."},"project_id":{"type":["string","null"],"description":"Move to a different project (UUID) or null to remove from project."},"context_ids":{"type":"array","items":{"type":"string"},"description":"Replace all context assignments. Pass empty array to remove all contexts."},"verification_items":{"type":["array","null"],"maxItems":50,"description":"Replace the whole checklist (max 50 items). Entries are label strings or {id?, label, checked?} objects. null or [] clears it. Cannot be combined with verification_items_add / verification_items_remove_indices.","items":{"oneOf":[{"type":"string","description":"Item label; starts unchecked."},{"type":"object","required":["label"],"properties":{"id":{"type":"string","description":"Optional stable id (max 64 chars); generated as v1, v2, ... when omitted."},"label":{"type":"string","description":"Item text (max 500 chars)."},"checked":{"type":"boolean","description":"Defaults to false."}}}]}},"verification_items_add":{"type":"array","description":"Append items to the current checklist (applied after verification_items_remove_indices). Same entry format as verification_items.","items":{"oneOf":[{"type":"string","description":"Item label; starts unchecked."},{"type":"object","required":["label"],"properties":{"id":{"type":"string","description":"Optional stable id (max 64 chars); generated as v1, v2, ... when omitted."},"label":{"type":"string","description":"Item text (max 500 chars)."},"checked":{"type":"boolean","description":"Defaults to false."}}}]}},"verification_items_remove_indices":{"type":"array","items":{"type":"integer","minimum":0},"description":"0-based positions in the current checklist to remove."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"priority":{"type":["string","null"]},"effort":{"type":["string","null"]},"estimated_minutes":{"type":["number","null"]},"due_date":{"type":["string","null"]},"project":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"contexts":{"type":"array","items":{"type":"string"}},"verification_items":{"type":"array","items":{"type":"object","required":["id","label","checked"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"checked":{"type":"boolean"}}}},"verification":{"type":["object","null"],"description":"Checklist progress (checked of total); null when the task has no checklist.","properties":{"checked":{"type":"number"},"total":{"type":"number"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/:task_id/archive","method":"POST","description":"Archive (soft-delete) a task. Sets status to 'archived'. The task can be restored later by updating its status back to another value (e.g. backlog). This is a convenience wrapper — equivalent to calling tasks_update with status='archived'.","outputSchema":{"type":"object","properties":{"task_id":{"type":"string"},"name":{"type":"string"},"previous_status":{"type":"string"},"status":{"type":"string","enum":["archived"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/schedule","method":"GET","description":"View the task schedule for a date range. Returns tasks grouped by date, plus overdue and optionally unscheduled tasks. Each task includes time-slot fields (starts_at, ends_at, time_block) from task_plans if scheduled to a specific time. Common uses: 'What does my week look like?' → start_date=2025-02-24&end_date=2025-02-28. 'What's scheduled for tomorrow?' → start_date=tomorrow&end_date=tomorrow. Overdue tasks (past due, not completed) are included by default.","inputSchema":{"type":"object","required":["start_date","end_date"],"properties":{"start_date":{"type":"string","format":"date","description":"Start of date range (YYYY-MM-DD, required)."},"end_date":{"type":"string","format":"date","description":"End of date range (YYYY-MM-DD, required)."},"include_overdue":{"type":"string","enum":["true","false"],"description":"Include overdue tasks from before start_date. Default: true."},"include_unscheduled":{"type":"string","enum":["true","false"],"description":"Include in_progress/planned/backlog tasks with no due date. Default: false."}}},"outputSchema":{"type":"object","properties":{"start_date":{"type":"string"},"end_date":{"type":"string"},"dates":{"type":"object","description":"Tasks grouped by date (YYYY-MM-DD keys). Empty dates are included.","additionalProperties":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"priority":{"type":"string"},"due_date":{"type":["string","null"]},"starts_at":{"type":["string","null"]},"ends_at":{"type":["string","null"]},"time_block":{"type":["string","null"]}}}}},"overdue":{"type":"array","description":"Tasks past their due date that are not completed."},"unscheduled":{"type":"array","description":"Active tasks with no due date (if requested)."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/:task_id/schedule","method":"POST","description":"Schedule or unschedule a task. Three modes: (1) Day-anchored: pass { date: 'YYYY-MM-DD' } to set both start_date and due_date to the same date — this is how the Tasks app represents 'scheduled for a specific day'. (2) Date range: pass { start_date: '...', due_date: '...' } for multi-day tasks. (3) Unschedule: pass { unschedule: true } to clear all dates. Optionally include starts_at and ends_at (HH:MM) to assign the task to a specific time slot on the scheduled date. Day, range, and time-slot scheduling move backlog/inbox/ready tasks to planned; other statuses are preserved. Atomic: when the task moves, its open slot on the old date is moved to the new date (date-only call keeps the time) or replaced by the new slot (call with times); if anything fails nothing is saved. Errors: 400 INVALID_DATE_RANGE / TIME_SLOT_INCOMPLETE / TIME_SLOT_ORDER / TIME_SLOT_NEEDS_DATE / INVALID_DATE, 404 NOT_FOUND, 500 SCHEDULE_FAILED.","inputSchema":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"Day-anchored scheduling: sets start_date = due_date = this date (YYYY-MM-DD)."},"start_date":{"type":["string","null"],"description":"Start date for range scheduling (YYYY-MM-DD)."},"due_date":{"type":["string","null"],"description":"Due date for range scheduling (YYYY-MM-DD)."},"unschedule":{"type":"boolean","description":"Set to true to clear all dates (unschedule the task)."},"starts_at":{"type":"string","description":"Optional time-slot start (HH:MM, e.g. '09:00'). Must be paired with ends_at. Creates a task_plans row for the scheduled date."},"ends_at":{"type":"string","description":"Optional time-slot end (HH:MM, e.g. '10:30'). Must be paired with starts_at."}}},"outputSchema":{"type":"object","properties":{"task_id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"start_date":{"type":["string","null"]},"due_date":{"type":["string","null"]},"is_day_anchored":{"type":"boolean"},"unscheduled":{"type":"boolean"},"starts_at":{"type":["string","null"]},"plan_date":{"type":["string","null"]},"ends_at":{"type":["string","null"]},"time_block":{"type":["string","null"]},"moved_slot_from":{"type":["string","null"]},"removed_slot_dates":{"type":"array","items":{"type":"string"}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/bulk-archive","method":"POST","description":"Archive multiple tasks at once. Use during cleanup sessions to archive completed or stale tasks in bulk. Accepts up to 50 task IDs per call. Respects MCP context exclusions.","inputSchema":{"type":"object","required":["task_ids"],"properties":{"task_ids":{"type":"array","items":{"type":"string"},"description":"Array of task UUIDs to archive (max 50)."}}},"outputSchema":{"type":"object","properties":{"archived_count":{"type":"number"},"skipped_ids":{"type":"array","items":{"type":"string"},"description":"Task IDs skipped (not found, not owned, or in an MCP-excluded context)."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/bulk-update","method":"POST","description":"Update multiple tasks at once with the same field values. Use to batch-set status, priority, due dates, effort, estimated_minutes, or move tasks to a project. Accepts up to 50 task IDs per call. Respects MCP context exclusions.","inputSchema":{"type":"object","required":["task_ids"],"properties":{"task_ids":{"type":"array","items":{"type":"string"},"description":"Array of task UUIDs to update (max 50)."},"status":{"type":"string","description":"Set status on all tasks."},"priority":{"type":"string","description":"Set priority on all tasks."},"due_date":{"type":"string","description":"Set due_date (YYYY-MM-DD). Pass null to clear."},"start_date":{"type":"string","description":"Set start_date (YYYY-MM-DD). Pass null to clear."},"project_id":{"type":"string","description":"Move all tasks to this project. Pass null to remove."},"effort":{"type":["string","null"],"enum":["trivial","moderate","deep",null],"description":"Set effort level on all tasks, or null to clear."},"estimated_minutes":{"type":["number","null"],"description":"Set estimated time in minutes on all tasks, or null to clear."}}},"outputSchema":{"type":"object","properties":{"updated_count":{"type":"number"},"skipped_ids":{"type":"array","items":{"type":"string"},"description":"Task IDs skipped (not found, not owned, or in an MCP-excluded context)."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/:task_id/comments","method":"GET","description":"List the comments on a task, oldest first. Comments are free-text notes on a task's activity timeline. Returns 404 if the task isn't yours or is in an MCP-excluded context.","outputSchema":{"type":"object","properties":{"task_id":{"type":"string"},"total":{"type":"number"},"comments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"task_id":{"type":"string"},"user_id":{"type":"string"},"content":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/:task_id/comments","method":"POST","description":"Add a comment to a task, authored by the calling user. Returns the created comment. Returns 404 if the task isn't yours or is in an MCP-excluded context.","inputSchema":{"type":"object","required":["content"],"properties":{"content":{"type":"string","description":"The comment text (non-empty)."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"task_id":{"type":"string"},"user_id":{"type":"string"},"content":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/:task_id/comments/:comment_id","method":"PATCH","description":"Edit the content of your own comment on a task and bump updated_at. Returns the updated comment. Returns 404 if the task or comment isn't yours.","inputSchema":{"type":"object","required":["content"],"properties":{"content":{"type":"string","description":"The new comment text (non-empty)."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"task_id":{"type":"string"},"user_id":{"type":"string"},"content":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/:task_id/comments/:comment_id","method":"DELETE","description":"Delete your own comment on a task. Author-only. Returns 404 if the task or comment isn't yours.","outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"comment_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/:task_id/pomodoro-sessions","method":"GET","description":"List a task's pomodoro/focus sessions (newest first) plus a 'time tracked' aggregate. total_minutes sums duration over finished sessions (those with completed_at set, whether completed or abandoned). Returns 404 if the task isn't yours or is in an MCP-excluded context.","outputSchema":{"type":"object","properties":{"task_id":{"type":"string"},"total_minutes":{"type":"number","description":"Sum of duration_minutes over sessions with completed_at set."},"completed_session_count":{"type":"number","description":"Count of sessions with completed=true."},"session_count":{"type":"number"},"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"task_id":{"type":"string"},"user_id":{"type":"string"},"started_at":{"type":"string"},"completed_at":{"type":["string","null"]},"duration_minutes":{"type":"number"},"completed":{"type":"boolean","description":"True only for a session the user completed. A finished-but-abandoned session has completed=false with completed_at set."},"start_comment":{"type":["string","null"]},"end_comment":{"type":["string","null"]},"created_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/tasks/:task_id/pomodoro-sessions","method":"POST","description":"Log a pomodoro/focus session on a task. Supports logging a COMPLETED session in one call (pass completed=true with started_at/duration_minutes/end_comment — the shape the on-device timer POSTs) or opening a started session (completed omitted/false). user_id is set from the caller. Returns 404 if the task isn't yours or is in an MCP-excluded context.","inputSchema":{"type":"object","properties":{"started_at":{"type":"string","description":"ISO timestamp the session started. Defaults to now()."},"duration_minutes":{"type":"number","description":"Session length in minutes. Defaults to 25."},"completed":{"type":"boolean","description":"True to log a completed session. Defaults to false."},"completed_at":{"type":["string","null"],"description":"ISO finish timestamp. Defaults to now() when completed=true and omitted; otherwise null."},"start_comment":{"type":["string","null"]},"end_comment":{"type":["string","null"]}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"task_id":{"type":"string"},"user_id":{"type":"string"},"started_at":{"type":"string"},"completed_at":{"type":["string","null"]},"duration_minutes":{"type":"number"},"completed":{"type":"boolean","description":"True only for a session the user completed. A finished-but-abandoned session has completed=false with completed_at set."},"start_comment":{"type":["string","null"]},"end_comment":{"type":["string","null"]},"created_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/:task_id/pomodoro-sessions/:session_id","method":"PATCH","description":"Finish or amend a pomodoro session. With no body it marks the session completed at now(). You can also pass completed, completed_at, duration_minutes, or end_comment. Returns 404 if the session or task isn't yours.","inputSchema":{"type":"object","properties":{"completed":{"type":"boolean","description":"Defaults to true (mark the session completed)."},"completed_at":{"type":["string","null"],"description":"ISO finish timestamp. Defaults to now()."},"duration_minutes":{"type":"number"},"end_comment":{"type":["string","null"]}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"task_id":{"type":"string"},"user_id":{"type":"string"},"started_at":{"type":"string"},"completed_at":{"type":["string","null"]},"duration_minutes":{"type":"number"},"completed":{"type":"boolean","description":"True only for a session the user completed. A finished-but-abandoned session has completed=false with completed_at set."},"start_comment":{"type":["string","null"]},"end_comment":{"type":["string","null"]},"created_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"tasks"},{"path":"/api/v1/tasks/settings/pomodoro","method":"GET","description":"Read the caller's pomodoro timer configuration from user_settings. Returns the column defaults (25/5/15/4/30) with is_default=true when the user has no settings row yet, so clients honor configured durations instead of hardcoding.","outputSchema":{"type":"object","properties":{"pomodoro_duration_minutes":{"type":"number"},"short_break_minutes":{"type":"number"},"long_break_minutes":{"type":"number"},"pomodoros_until_long_break":{"type":"number"},"session_threshold_minutes":{"type":"number"},"is_default":{"type":"boolean","description":"True when no user_settings row exists and defaults were returned."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"tasks"},{"path":"/api/v1/meals/recipes/create","method":"POST","description":"Parents only (children get 403). Create a new recipe with ingredients, steps, and optional tags. Use when a user shares a recipe in conversation and wants to save it to their recipe library. Handles ingredient deduplication automatically — if an ingredient already exists in the family's library, it will be reused rather than creating a duplicate. To add a hero image, call meals_recipes_image with the returned recipe_id (image_url is not accepted here).","inputSchema":{"type":"object","required":["title"],"properties":{"title":{"type":"string","description":"Recipe name."},"description":{"type":"string","description":"Brief description of the dish."},"servings":{"type":"number","default":4,"description":"Number of servings the recipe yields."},"prep_time_minutes":{"type":"number","description":"Preparation time in minutes."},"cook_time_minutes":{"type":"number","description":"Cooking time in minutes."},"source":{"type":"string","description":"Where the recipe came from (URL, book, etc.)."},"tags":{"type":"array","items":{"type":"string"},"description":"Tags for categorization (e.g. 'pasta', 'quick', 'vegetarian')."},"ingredients":{"type":"array","items":{"type":"object","required":["name","quantity","unit"],"properties":{"name":{"type":"string","description":"Ingredient name."},"quantity":{"type":"number","description":"Amount needed."},"unit":{"type":"string","description":"Unit of measurement (g, ml, cup, tbsp, etc.)."},"notes":{"type":"string","description":"Preparation notes (e.g. 'diced', 'room temperature')."},"category":{"type":"string","description":"Ingredient category (e.g. 'Produce', 'Dairy', 'Meat')."}}},"description":"List of ingredients with quantities."},"steps":{"type":"array","items":{"type":"object","required":["instruction"],"properties":{"instruction":{"type":"string","description":"Step instruction text."}}},"description":"Ordered cooking instructions."}}},"outputSchema":{"type":"object","properties":{"recipe_id":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","description":"Direct link to the recipe in the Meals app."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/recipes","method":"GET","description":"Search and list recipes from the user's recipe library. Supports text search on title, filtering by favorites, and filtering by tag. Use to find recipes when planning meals or answering questions about what's in the library.","inputSchema":{"type":"object","properties":{"search":{"type":"string","description":"Text search on recipe title (case-insensitive partial match)."},"favorites_only":{"type":"boolean","description":"If true, only return favorited recipes."},"tag":{"type":"string","description":"Filter by tag (exact match)."},"include_cost":{"type":"boolean","description":"If true, include cost estimates (total_cost, cost_per_serving) for each recipe using ingredient pricing data."}}},"outputSchema":{"type":"object","properties":{"recipes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"servings":{"type":"number"},"prep_time_minutes":{"type":["number","null"]},"cook_time_minutes":{"type":["number","null"]},"is_favorite":{"type":"boolean"},"tags":{"type":"array","items":{"type":"string"}},"active_version_number":{"type":["number","null"],"description":"Version number of the currently active recipe version. Null if no versions exist."},"family_rating":{"type":["number","null"],"description":"Average rating across all family members for all times this recipe was cooked. Null if no ratings."},"would_eat_again_pct":{"type":["number","null"],"description":"Percentage of 'would eat again' responses across all family members. Null if no responses."},"total_cost":{"type":["number","null"],"description":"Total estimated cost in ISK (only when include_cost=true)."},"cost_per_serving":{"type":["number","null"],"description":"Cost per serving in ISK (only when include_cost=true)."},"priced_ingredient_count":{"type":["number","null"],"description":"Number of ingredients with pricing data (only when include_cost=true)."},"unpriced_ingredient_count":{"type":["number","null"],"description":"Number of ingredients without pricing data (only when include_cost=true)."}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id/feedback-summary","method":"GET","description":"Get aggregated feedback for a recipe across all times it was cooked. Shows per-person breakdown (average rating, consumption habits, would-eat-again percentage) and overall totals. Optionally filter by version or compare feedback across all versions. Use to answer 'How does the family feel about this recipe?' or to compare recipe versions.","inputSchema":{"type":"object","properties":{"version_id":{"type":"string","description":"Optional version UUID to filter feedback to a specific recipe version only."},"compare_versions":{"type":"boolean","description":"If true, return feedback grouped by version with per-version breakdowns instead of aggregate."}}},"outputSchema":{"type":"object","properties":{"recipe_id":{"type":"string"},"recipe_title":{"type":"string"},"times_cooked":{"type":"number","description":"Number of distinct meal plans that used this recipe."},"per_person":{"type":"array","items":{"type":"object","properties":{"person_id":{"type":"string"},"person_name":{"type":"string"},"average_rating":{"type":["number","null"],"description":"Average star rating (1 decimal place)."},"would_eat_again_pct":{"type":["number","null"],"description":"Percentage of times they said yes to would_eat_again."},"consumption_breakdown":{"type":"object","properties":{"ate_well":{"type":"number"},"ate_some":{"type":"number"},"didnt_eat":{"type":"number"}}},"feedback_count":{"type":"number"}}}},"overall":{"type":["object","null"],"properties":{"average_rating":{"type":["number","null"]},"would_eat_again_pct":{"type":["number","null"]},"consumption_breakdown":{"type":"object","properties":{"ate_well":{"type":"number"},"ate_some":{"type":"number"},"didnt_eat":{"type":"number"}}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id","method":"GET","description":"Get full recipe details including ingredients with nutrition data, step-by-step instructions, tags, version history, and a calculated nutrition summary per serving. By default returns the active version's ingredients and steps. Pass version_id to view a specific version.","inputSchema":{"type":"object","properties":{"version_id":{"type":"string","description":"Optional UUID of a specific version to view. If omitted, the active version is used."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"servings":{"type":"number"},"prep_time_minutes":{"type":["number","null"]},"cook_time_minutes":{"type":["number","null"]},"total_time_minutes":{"type":["number","null"]},"source":{"type":["string","null"]},"is_favorite":{"type":"boolean"},"image_url":{"type":["string","null"],"description":"Relative storage path in the recipe-images private bucket, or null if no image. Use image_signed_url for display."},"image_signed_url":{"type":["string","null"],"description":"Time-limited signed URL for displaying the image (valid for 1 hour), or null if no image."},"tags":{"type":"array","items":{"type":"string"}},"steps":{"type":"array","items":{"type":"object","properties":{"step_number":{"type":"number"},"instruction":{"type":"string"}}}},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"},"notes":{"type":["string","null"]},"category":{"type":["string","null"]}}}},"nutrition":{"type":"object","description":"Calculated nutrition per serving (when ingredient data is available).","properties":{"calories":{"type":"number"},"protein_g":{"type":"number"},"carbs_g":{"type":"number"},"fat_g":{"type":"number"},"fiber_g":{"type":"number"}}},"cost":{"type":["object","null"],"description":"Cost estimate in ISK based on ingredient pricing data.","properties":{"total_cost":{"type":"number","description":"Total recipe cost in ISK."},"cost_per_serving":{"type":"number","description":"Per-serving cost in ISK."},"priced_ingredient_count":{"type":"number"},"unpriced_ingredient_count":{"type":["number","null"]}}},"active_version":{"type":["object","null"],"description":"The currently active version (or the requested version if version_id was passed).","properties":{"id":{"type":"string"},"version_number":{"type":"number"},"change_notes":{"type":["string","null"]},"created_at":{"type":"string"}}},"versions":{"type":"array","description":"Summary of all versions for this recipe, newest first.","items":{"type":"object","properties":{"id":{"type":"string"},"version_number":{"type":"number"},"change_notes":{"type":["string","null"]},"created_at":{"type":"string"}}}},"url":{"type":"string"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id/versions/:version_id","method":"GET","description":"Get full details of a specific recipe version including resolved ingredient names and step-by-step instructions. Use to compare versions or view historical recipe data.","outputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Version UUID."},"recipe_id":{"type":"string"},"recipe_title":{"type":"string"},"version_number":{"type":"number"},"servings":{"type":"number"},"prep_time_minutes":{"type":["number","null"]},"cook_time_minutes":{"type":["number","null"]},"change_notes":{"type":["string","null"]},"created_at":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"step_number":{"type":"number"},"instruction":{"type":"string"}}}},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"},"display_order":{"type":"number"},"category":{"type":["string","null"]}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id/versions","method":"POST","description":"Parents only (children get 403). Create a new version of a recipe with updated ingredients, steps, and/or timing. The new version becomes the active version automatically. Use when a user wants to modify a recipe while preserving its history. The version number auto-increments.","inputSchema":{"type":"object","properties":{"ingredients":{"type":"array","description":"Full replacement ingredient list for this version.","items":{"type":"object","properties":{"name":{"type":"string","description":"Ingredient name (will be matched or created in the family library)."},"quantity":{"type":"number"},"unit":{"type":"string"},"category":{"type":"string","description":"Optional ingredient category."}},"required":["name","quantity","unit"]}},"steps":{"type":"array","description":"Full replacement step list for this version.","items":{"type":"object","properties":{"instruction":{"type":"string"}},"required":["instruction"]}},"servings":{"type":"number","description":"Servings for this version. Defaults to current recipe servings."},"prep_time_minutes":{"type":"number","description":"Prep time for this version."},"cook_time_minutes":{"type":"number","description":"Cook time for this version."},"change_notes":{"type":"string","description":"Description of what changed in this version."}}},"outputSchema":{"type":"object","properties":{"version_id":{"type":"string"},"version_number":{"type":"number"},"recipe_id":{"type":"string"},"change_notes":{"type":["string","null"]},"created_at":{"type":"string"},"ingredients_count":{"type":"number"},"steps_count":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id/rollback","method":"POST","description":"Parents only (children get 403). Roll back a recipe to a previous version. Sets the specified version as the active version without deleting any newer versions. Use when a user wants to revert recipe changes.","inputSchema":{"type":"object","properties":{"version_id":{"type":"string","description":"UUID of the version to roll back to."}},"required":["version_id"]},"outputSchema":{"type":"object","properties":{"recipe_id":{"type":"string"},"active_version_id":{"type":"string"},"active_version_number":{"type":"number"},"change_notes":{"type":["string","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id","method":"PATCH","description":"Parents only (children get 403). Update an existing recipe. All fields are optional — only the fields you provide will be updated. If 'ingredients', 'steps', or 'tags' are provided, they fully replace the existing set. Changing ingredients, steps, servings, or prep/cook time saves a new recipe version and makes it active (earlier versions stay available for rollback); unchanged parts carry over from the current version. Returns the full updated recipe with nutrition and cost data. Use when a user wants to edit a recipe's title, description, timing, or replace its ingredients/steps/tags.","inputSchema":{"type":"object","properties":{"title":{"type":"string","description":"Updated recipe name."},"change_notes":{"type":["string","null"],"description":"Optional note describing the change, stored on the new version when ingredients, steps, servings or times change."},"description":{"type":["string","null"],"description":"Updated description of the dish."},"servings":{"type":["number","null"],"description":"Updated number of servings."},"prep_time_minutes":{"type":["number","null"],"description":"Updated preparation time in minutes."},"cook_time_minutes":{"type":["number","null"],"description":"Updated cooking time in minutes."},"source":{"type":["string","null"],"description":"Updated source (URL, book, etc.)."},"source_url":{"type":["string","null"],"description":"Updated direct URL to the original recipe."},"notes":{"type":["string","null"],"description":"Updated recipe notes."},"image_url":{"type":["string","null"],"description":"null to clear the image, or exactly recipes/{recipe_id}/{uuid}.{jpg|jpeg|png|webp|heic} for this recipe (the storage_path returned by meals_recipes_image). Any other value is rejected with 400. Prefer meals_recipes_image / meals_recipes_image_delete."},"ingredients":{"type":"array","description":"If provided, fully replaces all ingredients. Each item must include name, quantity, and unit.","items":{"type":"object","required":["name","quantity","unit"],"properties":{"name":{"type":"string","description":"Ingredient name."},"quantity":{"type":"number","description":"Amount needed."},"unit":{"type":"string","description":"Unit of measurement (g, ml, cup, tbsp, etc.)."},"notes":{"type":"string","description":"Preparation notes (e.g. 'diced', 'room temperature')."},"category":{"type":"string","description":"Ingredient category (e.g. 'Produce', 'Dairy', 'Meat')."}}}},"steps":{"type":"array","description":"If provided, fully replaces all steps. Steps are numbered in order.","items":{"type":"object","required":["instruction"],"properties":{"instruction":{"type":"string","description":"Step instruction text."}}}},"tags":{"type":"array","items":{"type":"string"},"description":"If provided, fully replaces all tags. Tag names are lowercased and trimmed automatically."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"servings":{"type":"number"},"prep_time_minutes":{"type":["number","null"]},"cook_time_minutes":{"type":["number","null"]},"total_time_minutes":{"type":["number","null"]},"source":{"type":["string","null"]},"is_favorite":{"type":"boolean"},"image_url":{"type":["string","null"],"description":"Relative storage path in the recipe-images private bucket, or null if no image. Use image_signed_url for display."},"image_signed_url":{"type":["string","null"],"description":"Time-limited signed URL for displaying the image (valid for 1 hour), or null if no image."},"tags":{"type":"array","items":{"type":"string"}},"steps":{"type":"array","items":{"type":"object","properties":{"step_number":{"type":"number"},"instruction":{"type":"string"}}}},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"},"notes":{"type":["string","null"]},"category":{"type":["string","null"]}}}},"nutrition":{"type":["object","null"],"description":"Calculated nutrition per serving (when ingredient data is available).","properties":{"calories":{"type":"number"},"protein_g":{"type":"number"},"carbs_g":{"type":"number"},"fat_g":{"type":"number"},"fiber_g":{"type":"number"}}},"cost":{"type":["object","null"],"description":"Cost estimate in ISK based on ingredient pricing data.","properties":{"total_cost":{"type":"number"},"cost_per_serving":{"type":"number"},"priced_ingredient_count":{"type":"number"},"unpriced_ingredient_count":{"type":["number","null"]}}},"url":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/plan/week","method":"GET","description":"Get the meal plan for a given week, showing all planned meals grouped by date and meal type. Includes recipe titles and attendee names so you can describe the full weekly plan without follow-up calls. Use to answer 'What's for dinner this week?' or review the current plan.","inputSchema":{"type":"object","properties":{"week_start":{"type":"string","format":"date","description":"Monday of the week to fetch (YYYY-MM-DD). Defaults to current week."}}},"outputSchema":{"type":"object","properties":{"week_start":{"type":"string","format":"date"},"week_end":{"type":"string","format":"date"},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"meal_date":{"type":"string","format":"date"},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"]},"meal_source":{"type":"string","enum":["home_cooked","dining_out","takeout","invited"]},"servings":{"type":"number"},"recipes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"}}}},"attendees":{"type":"array","items":{"type":"object","properties":{"family_member_id":{"type":"string"},"display_name":{"type":"string"},"description":{"type":["string","null"]},"meal_character":{"type":"array","items":{"type":"string"}}}}},"location":{"type":["string","null"]},"estimated_cost":{"type":["number","null"]},"notes":{"type":["string","null"]}}}},"summary":{"type":"object","properties":{"total_meals":{"type":"number"},"home_cooked":{"type":"number"},"dining_out":{"type":"number"},"empty_slots":{"type":"number"}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/plan/add","method":"POST","description":"Add a meal to the weekly plan. Supports home-cooked meals with recipe references, as well as dining out, takeout, and invited meals with location and cost tracking. For home-cooked meals, automatically creates person_meals entries for each attendee (linked to the meal plan). Each date+meal_type slot can only have one plan — use this to fill empty slots.","inputSchema":{"type":"object","required":["meal_date","meal_type","meal_source"],"properties":{"meal_date":{"type":"string","format":"date","description":"Date for the meal (YYYY-MM-DD)."},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"],"description":"Which meal of the day."},"meal_source":{"type":"string","enum":["home_cooked","dining_out","takeout","invited","school","work","packed_lunch","other","leftover"],"description":"How the meal will be sourced. Use 'leftover' to plan a meal from existing leftovers — requires leftover_id or recipe_id to identify which leftover to consume."},"recipe_ids":{"type":"array","items":{"type":"string"},"description":"Recipe IDs for home-cooked meals. Can include multiple recipes per meal."},"leftover_id":{"type":"string","description":"ID of the leftover to consume (for meal_source='leftover'). If omitted, auto-picks the most urgent leftover matching recipe_id."},"family_member_ids":{"type":"array","items":{"type":"string"},"description":"Family member IDs (from public.family_members) for meal attendees. If omitted for home-cooked meals, uses members marked as default in family preferences."},"location":{"type":"string","description":"Restaurant or location name (for dining out/takeout)."},"estimated_cost":{"type":"number","description":"Estimated cost in ISK (for dining out/takeout)."},"people_count":{"type":"number","description":"Number of people (for non-home-cooked meals)."},"servings":{"type":"number","description":"Number of servings to consume (for leftover meals, defaults to 1)."},"notes":{"type":"string","description":"Additional notes about the meal."}}},"outputSchema":{"type":"object","properties":{"meal_plan_id":{"type":"string"},"meal_date":{"type":"string"},"meal_type":{"type":"string"},"meal_source":{"type":"string"},"servings":{"type":"number"},"recipes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"}}}},"attendees":{"type":"array","items":{"type":"object","properties":{"family_member_id":{"type":"string"},"display_name":{"type":"string"},"servings":{"type":"number"}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/plan/:meal_plan_id","method":"DELETE","description":"Remove a meal from the plan, freeing up the time slot. Use when a user wants to change plans or clear a meal slot.","outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"meal_plan_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/shopping-list/generate","method":"POST","description":"Generate a shopping list from the meal plan for a date range. Aggregates all ingredients from home-cooked meals, scales quantities by serving ratios, and deduplicates by ingredient+unit. Any existing active shopping list is automatically completed first. Use when a user asks 'What do I need to buy this week?' or wants to prepare for grocery shopping.","inputSchema":{"type":"object","required":["start_date","end_date"],"properties":{"start_date":{"type":"string","format":"date","description":"Start of the date range (YYYY-MM-DD)."},"end_date":{"type":"string","format":"date","description":"End of the date range (YYYY-MM-DD)."},"name":{"type":"string","description":"Optional name for the shopping list. Defaults to 'Shopping list for [start] - [end]'."}}},"outputSchema":{"type":"object","properties":{"shopping_list_id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"start_date":{"type":"string","format":"date"},"end_date":{"type":"string","format":"date"},"item_count":{"type":"number"},"items_by_category":{"type":"object","description":"Items grouped by category (e.g. Produce, Dairy, Meat). Each category contains an array of { name, quantity, unit }.","additionalProperties":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"}}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/shopping-list","method":"GET","description":"Get the currently active shopping list with all items grouped by category. Includes ingredient names, quantities, units, and purchased status. Returns null if no active list exists. Use to read back the shopping list to the user or check what still needs to be bought.","outputSchema":{"type":"object","properties":{"shopping_list":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"start_date":{"type":["string","null"],"format":"date"},"end_date":{"type":["string","null"],"format":"date"},"created_at":{"type":"string"}}},"summary":{"type":"object","properties":{"total_items":{"type":"number"},"purchased":{"type":"number"},"remaining":{"type":"number"}}},"items_by_category":{"type":"object","description":"Items grouped by category. Each category contains an array of items with id, name, quantity, unit, is_purchased, and notes.","additionalProperties":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"},"is_purchased":{"type":"boolean"},"notes":{"type":["string","null"]}}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/person-meals","method":"POST","description":"Log what a specific family member ate for a meal slot. Creates or updates a person_meal entry (upserts on family_member + date + meal_type). Use to track what kids ate at school, what someone had for lunch at work, or to record any individual meal. The description is free-text (e.g. 'kjúklingasúpa') and meal_character tags describe the meal's nature (e.g. 'light', 'soup', 'protein-heavy').","inputSchema":{"type":"object","required":["family_member_id","meal_date","meal_type","meal_source"],"properties":{"family_member_id":{"type":"string","description":"ID of the family member (from public.family_members)."},"meal_date":{"type":"string","format":"date","description":"Date of the meal (YYYY-MM-DD)."},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"],"description":"Which meal of the day."},"meal_source":{"type":"string","enum":["home_cooked","school","work","dining_out","takeout","invited","packed_lunch","other"],"description":"Where/how the meal was sourced. Use 'school' for kids' school lunches, 'work' for workplace canteen, etc."},"description":{"type":"string","description":"Free-text description of what was eaten (e.g. 'kjúklingasúpa', 'pasta with meatballs')."},"meal_character":{"type":"array","items":{"type":"string"},"description":"Tags describing the meal's character: protein-heavy, light, soup, carb-heavy, salad, fish, comfort-food, vegetarian."},"meal_plan_id":{"type":"string","description":"Link to a home-cooked meal plan (if this person_meal comes from a planned meal)."},"location":{"type":"string","description":"Where the meal was eaten (e.g. school name, restaurant)."},"notes":{"type":"string","description":"Additional notes."},"rating":{"type":"number","description":"Star rating 1-5. Nullable — not all family members can express preference."},"consumption":{"type":"string","enum":["ate_well","ate_some","didnt_eat"],"description":"How much was eaten. Designed for young children but usable by anyone."},"feedback_notes":{"type":"string","description":"Short comment about the meal (e.g. 'too salty', 'loved the sauce')."},"would_eat_again":{"type":"boolean","description":"Quick yes/no for meal planning — most actionable feedback field."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"family_member_id":{"type":"string"},"display_name":{"type":"string"},"meal_date":{"type":"string","format":"date"},"meal_type":{"type":"string"},"meal_source":{"type":"string"},"description":{"type":["string","null"]},"meal_character":{"type":"array","items":{"type":"string"}},"meal_plan_id":{"type":["string","null"]},"location":{"type":["string","null"]},"notes":{"type":["string","null"]},"rating":{"type":["number","null"]},"consumption":{"type":["string","null"]},"feedback_notes":{"type":["string","null"]},"would_eat_again":{"type":["boolean","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/person-meals/day","method":"GET","description":"Get all person-level meals for a given date, grouped by meal type (breakfast, lunch, dinner, snack). Shows what each family member ate, with meal source, description, character tags, and linked meal plan info. This is THE key planning endpoint — use it to see what everyone had for lunch before planning dinner.","inputSchema":{"type":"object","required":["date"],"properties":{"date":{"type":"string","format":"date","description":"Date to fetch (YYYY-MM-DD)."}}},"outputSchema":{"type":"object","properties":{"date":{"type":"string","format":"date"},"breakfast":{"type":"array","description":"Person meals for breakfast."},"lunch":{"type":"array","description":"Person meals for lunch."},"dinner":{"type":"array","description":"Person meals for dinner."},"snack":{"type":"array","description":"Person meals for snacks."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/person-meals/batch","method":"POST","description":"Log meals for multiple family members at once. Ideal for recording school lunches for several kids in a single call — provide the same description and source for multiple family_member_ids. Each entry is upserted independently. Maximum 20 entries per batch.","inputSchema":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","items":{"type":"object","required":["family_member_id","meal_date","meal_type","meal_source"],"properties":{"family_member_id":{"type":"string","description":"Family member ID (from public.family_members)."},"meal_date":{"type":"string","format":"date","description":"Date (YYYY-MM-DD)."},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"]},"meal_source":{"type":"string","enum":["home_cooked","school","work","dining_out","takeout","invited","packed_lunch","other"]},"description":{"type":"string"},"meal_character":{"type":"array","items":{"type":"string"}},"rating":{"type":"number","description":"Star rating 1-5."},"consumption":{"type":"string","enum":["ate_well","ate_some","didnt_eat"],"description":"How much was eaten."},"feedback_notes":{"type":"string","description":"Short comment about the meal."},"would_eat_again":{"type":"boolean","description":"Quick yes/no for meal planning."}}},"description":"Array of person_meal entries to create/update. Maximum 20 per batch."}}},"outputSchema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"family_member_id":{"type":"string"},"display_name":{"type":"string"},"meal_date":{"type":"string"},"meal_type":{"type":"string"},"meal_source":{"type":"string"},"description":{"type":["string","null"]},"meal_character":{"type":"array","items":{"type":"string"}},"rating":{"type":["number","null"]},"consumption":{"type":["string","null"]},"feedback_notes":{"type":["string","null"]},"would_eat_again":{"type":["boolean","null"]}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/person-meals/:id/feedback","method":"PATCH","description":"Add or update feedback on an existing person meal. Use after eating to rate the meal — accepts any subset of feedback fields. This is the 'rate after eating' endpoint.","inputSchema":{"type":"object","properties":{"rating":{"type":"number","description":"Star rating 1-5. Nullable — not all family members can express preference."},"consumption":{"type":"string","enum":["ate_well","ate_some","didnt_eat"],"description":"How much was eaten. Designed for young children but usable by anyone."},"feedback_notes":{"type":"string","description":"Short comment about the meal (e.g. 'too salty', 'loved the sauce')."},"would_eat_again":{"type":"boolean","description":"Quick yes/no for meal planning — most actionable feedback field."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"family_member_id":{"type":"string"},"display_name":{"type":"string"},"meal_date":{"type":"string","format":"date"},"meal_type":{"type":"string"},"meal_source":{"type":"string"},"description":{"type":["string","null"]},"meal_character":{"type":"array","items":{"type":"string"}},"meal_plan_id":{"type":["string","null"]},"location":{"type":["string","null"]},"notes":{"type":["string","null"]},"rating":{"type":["number","null"]},"consumption":{"type":["string","null"]},"feedback_notes":{"type":["string","null"]},"would_eat_again":{"type":["boolean","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/person-meals/feedback/batch","method":"POST","description":"Rate meals for multiple family members in one call. Ideal for after dinner — record everyone's reaction in a single request. Each entry targets an existing person_meal by ID and accepts any subset of feedback fields. Maximum 20 entries per batch.","inputSchema":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","items":{"type":"object","required":["person_meal_id"],"properties":{"person_meal_id":{"type":"string","description":"ID of the person_meal to add feedback to."},"rating":{"type":"number","description":"Star rating 1-5."},"consumption":{"type":"string","enum":["ate_well","ate_some","didnt_eat"],"description":"How much was eaten."},"feedback_notes":{"type":"string","description":"Short comment about the meal."},"would_eat_again":{"type":"boolean","description":"Quick yes/no for meal planning."}}},"description":"Array of feedback entries. Each must reference an existing person_meal_id. Maximum 20 per batch."}}},"outputSchema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"family_member_id":{"type":"string"},"display_name":{"type":"string"},"meal_date":{"type":"string"},"meal_type":{"type":"string"},"meal_source":{"type":"string"},"rating":{"type":["number","null"]},"consumption":{"type":["string","null"]},"feedback_notes":{"type":["string","null"]},"would_eat_again":{"type":["boolean","null"]}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/person-meals/:id","method":"DELETE","description":"Delete a single person meal entry. Use when a meal was logged incorrectly or needs to be removed.","outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/ingredients","method":"GET","description":"List all ingredients in the user's family library. Optionally include pricing data (reference price, unit, per-gram cost). Filter by category, search by name, or find ingredients that still need pricing. Use when reviewing ingredient prices or checking which items need price updates.","inputSchema":{"type":"object","properties":{"include_pricing":{"type":"boolean","description":"If true, include pricing columns (reference_price, reference_price_unit, price_per_gram, has_price)."},"has_price":{"type":"boolean","description":"Filter by pricing status — true for priced, false for unpriced. Only works when include_pricing=true."},"category":{"type":"string","description":"Filter by ingredient category (exact match)."},"search":{"type":"string","description":"Search ingredient name (case-insensitive partial match)."}}},"outputSchema":{"type":"object","properties":{"ingredients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"type":["string","null"]},"default_unit":{"type":["string","null"]},"reference_price":{"type":["number","null"]},"reference_price_unit":{"type":["string","null"]},"reference_price_quantity":{"type":["number","null"]},"has_price":{"type":"boolean"},"price_per_gram":{"type":["number","null"]}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/ingredients/:ingredient_id/price","method":"PATCH","description":"Update the reference price for a single ingredient. Prices are in ISK. The unit is automatically normalized to lowercase. Use when a user provides a grocery price for an ingredient.","inputSchema":{"type":"object","required":["reference_price","reference_price_unit"],"properties":{"reference_price":{"type":"number","description":"Price in ISK (non-negative integer)."},"reference_price_unit":{"type":"string","description":"Unit the price refers to (e.g. 'kg', 'g', 'ml', 'stk', 'pakki')."},"reference_price_quantity":{"type":"number","description":"How many of the unit the price covers (default 1). E.g. 500 for '500g'."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"reference_price":{"type":"number"},"reference_price_unit":{"type":"string"},"reference_price_quantity":{"type":"number"},"price_updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/ingredients/prices/bulk","method":"POST","description":"Bulk-update pricing for multiple ingredients at once. Maximum 50 items per batch. Use when importing prices from a grocery receipt or updating multiple items simultaneously.","inputSchema":{"type":"object","required":["prices"],"properties":{"prices":{"type":"array","items":{"type":"object","required":["ingredient_id","reference_price","reference_price_unit"],"properties":{"ingredient_id":{"type":"string","description":"ID of the ingredient to update."},"reference_price":{"type":"number","description":"Price in ISK (non-negative integer)."},"reference_price_unit":{"type":"string","description":"Unit the price refers to (e.g. 'kg', 'g', 'ml', 'stk')."},"reference_price_quantity":{"type":"number","description":"How many of the unit the price covers (default 1)."}}},"description":"Array of ingredient price updates. Maximum 50 per batch."}}},"outputSchema":{"type":"object","properties":{"updated_count":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/ingredients/create","method":"POST","description":"Create a new ingredient in the family library. Performs case-insensitive deduplication — if an ingredient with the same name already exists, returns it instead of creating a duplicate. Use when mapping receipt line items to ingredients or when a user wants to add a new ingredient.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Ingredient name."},"category":{"type":"string","description":"Category (e.g. 'dairy', 'produce')."},"default_unit":{"type":"string","description":"Default unit of measure (e.g. 'l', 'kg', 'stk')."},"calories_per_100g":{"type":"number","description":"Calories per 100g."},"protein_g_per_100g":{"type":"number","description":"Protein grams per 100g."},"carbs_g_per_100g":{"type":"number","description":"Carbs grams per 100g."},"fat_g_per_100g":{"type":"number","description":"Fat grams per 100g."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/ingredients/:ingredient_id/price-observations","method":"GET","description":"List price observations for an ingredient, sorted by observed_at descending. Shows price history from receipts, manual entries, and imports. Use when viewing an ingredient's price trend or checking historical prices.","inputSchema":{"type":"object","properties":{"limit":{"type":"integer","default":50,"description":"Max observations to return."},"offset":{"type":"integer","default":0,"description":"Pagination offset."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/ingredients/:ingredient_id/price-observations","method":"POST","description":"Record a price observation for an ingredient. The database trigger automatically updates the ingredient's reference_price if this is the most recent observation. Use when manually logging a price or when linking a receipt line item to an ingredient.","inputSchema":{"type":"object","required":["price","quantity","unit","observed_at","source"],"properties":{"price":{"type":"number","description":"Total price paid (e.g. 299 ISK)."},"quantity":{"type":"number","description":"Number of units (e.g. 1 for 1L)."},"unit":{"type":"string","description":"Unit of measure (l, kg, g, ml, stk, etc.)."},"observed_at":{"type":"string","description":"Date of purchase (YYYY-MM-DD)."},"source":{"type":"string","enum":["receipt","manual","import"],"description":"Where this price came from."},"source_line_item_id":{"type":"string","description":"Optional budgeting line item ID."},"merchant_id":{"type":"string","description":"Optional merchant ID."},"currency":{"type":"string","default":"ISK","description":"Currency code."},"notes":{"type":"string","description":"Optional notes."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id/cost","method":"GET","description":"Get the full cost breakdown for a recipe. Computes total cost and per-serving cost in ISK using ingredient reference prices and unit conversions. By default uses the active version's ingredients. Pass version_id to compute cost for a specific version.","inputSchema":{"type":"object","properties":{"version_id":{"type":"string","description":"Optional version UUID to compute cost for a specific version. Defaults to the active version."}}},"outputSchema":{"type":"object","properties":{"recipe_id":{"type":"string"},"title":{"type":"string"},"servings":{"type":"number"},"total_cost":{"type":"number","description":"Total estimated cost in ISK."},"cost_per_serving":{"type":"number","description":"Cost per serving in ISK."},"priced_ingredient_count":{"type":"number"},"unpriced_ingredient_count":{"type":["number","null"]},"unpriced_ingredients":{"type":"array","items":{"type":"string"},"description":"Names of ingredients without pricing data."},"cost_breakdown":{"type":"array","items":{"type":"object","properties":{"ingredient_name":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"},"ingredient_cost_isk":{"type":"number"}}},"description":"Per-ingredient cost breakdown."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/leftovers","method":"POST","description":"Log leftovers after a meal. Creates a leftover record with servings available and a use-by date. Use when a user says they have leftover food (e.g. 'We have 2 servings of pasta left over from dinner').","inputSchema":{"type":"object","required":["recipe_id","servings_remaining"],"properties":{"recipe_id":{"type":"string","description":"Recipe that was cooked."},"servings_remaining":{"type":"number","description":"How many servings are left over."},"source_meal_plan_id":{"type":"string","description":"Meal plan entry that produced these leftovers (optional)."},"use_by_date":{"type":"string","format":"date","description":"Use-by date (YYYY-MM-DD). Defaults to today + 3 days."},"notes":{"type":"string","description":"Storage notes (e.g. 'in blue container', 'in freezer')."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"recipe_id":{"type":"string"},"recipe_title":{"type":"string"},"servings_remaining":{"type":"number"},"servings_original":{"type":"number"},"created_date":{"type":"string","format":"date"},"use_by_date":{"type":"string","format":"date"},"status":{"type":"string"},"notes":{"type":["string","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/leftovers","method":"GET","description":"List leftovers in the fridge. By default returns only active (available, not expired) leftovers sorted by urgency. Use to answer 'What leftovers do we have?' or check what needs to be eaten soon.","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["available","consumed","expired","discarded"],"description":"Filter by status. Default: available only."},"recipe_id":{"type":"string","description":"Filter by specific recipe."},"include_expired":{"type":"boolean","description":"Include expired/discarded leftovers for history."},"is_urgent":{"type":"boolean","description":"Only return leftovers expiring today or tomorrow. Useful for morning briefing integration."}}},"outputSchema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"recipe_id":{"type":"string"},"recipe_title":{"type":"string"},"servings_remaining":{"type":"number"},"servings_original":{"type":"number"},"created_date":{"type":"string","format":"date"},"use_by_date":{"type":"string","format":"date"},"days_until_expiry":{"type":"number"},"is_urgent":{"type":"boolean"},"notes":{"type":["string","null"]}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/leftovers/:id","method":"PATCH","description":"Update a leftover record. Use to mark leftovers as discarded ('we threw it away'), update notes, adjust servings, or change the use-by date.","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["available","consumed","expired","discarded"],"description":"New status. Use 'discarded' when food was thrown away."},"notes":{"type":"string","description":"Updated storage notes."},"use_by_date":{"type":"string","format":"date","description":"Updated use-by date (YYYY-MM-DD)."},"servings_remaining":{"type":"number","description":"Manual adjustment of remaining servings."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"recipe_id":{"type":"string"},"servings_remaining":{"type":"number"},"servings_original":{"type":"number"},"created_date":{"type":"string","format":"date"},"use_by_date":{"type":"string","format":"date"},"status":{"type":"string"},"notes":{"type":["string","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/leftovers/:id/consume","method":"POST","description":"Consume servings from a leftover. Use when someone eats leftover food (e.g. 'I had one serving of the leftover pasta for lunch'). Decrements remaining servings and auto-marks as consumed when depleted.","inputSchema":{"type":"object","required":["servings_consumed"],"properties":{"servings_consumed":{"type":"number","description":"Number of servings consumed."},"meal_plan_id":{"type":"string","description":"Link to a meal plan entry (if consuming as part of a planned meal)."}}},"outputSchema":{"type":"object","properties":{"consumption_id":{"type":"string"},"leftover_id":{"type":"string"},"servings_consumed":{"type":"number"},"servings_remaining":{"type":"number"},"leftover_status":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/leftovers/suggestions","method":"GET","description":"Get leftover-based meal suggestions for planning. Returns available leftovers grouped by recipe, sorted by urgency (expiring soonest first). Use when planning meals to surface 'You have leftovers!' suggestions, especially for lunch.","outputSchema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"recipe_id":{"type":"string"},"recipe_title":{"type":"string"},"total_servings":{"type":"number"},"most_urgent_use_by":{"type":"string","format":"date"},"is_urgent":{"type":"boolean"},"leftover_ids":{"type":"array","items":{"type":"string"}}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/leftovers/expire","method":"POST","description":"Trigger manual expiry check. Finds all available leftovers past their use-by date and marks them as expired. Returns the list of newly expired items. Called by the morning briefing or scheduled tasks.","outputSchema":{"type":"object","properties":{"expired_count":{"type":"number"},"expired_items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"recipe_id":{"type":"string"},"recipe_title":{"type":"string"},"servings_wasted":{"type":"number"},"use_by_date":{"type":"string","format":"date"}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/leftovers/waste","method":"GET","description":"Get food waste data with aggregate summary. Shows individual waste events (expired and discarded leftovers) with optional cost estimates. Use to answer 'How much food are we wasting?' or track waste trends.","inputSchema":{"type":"object","properties":{"period":{"type":"string","enum":["week","month","year","all"],"description":"Time period for waste data. Default: month."}}},"outputSchema":{"type":"object","properties":{"period":{"type":"string"},"summary":{"type":"object","properties":{"total_waste_events":{"type":"number"},"total_servings_wasted":{"type":"number"},"total_cost_wasted":{"type":["number","null"]}}},"events":{"type":"array","items":{"type":"object","properties":{"leftover_id":{"type":"string"},"recipe_id":{"type":"string"},"recipe_title":{"type":"string"},"servings_wasted":{"type":"number"},"status":{"type":"string"},"created_date":{"type":"string","format":"date"},"use_by_date":{"type":"string","format":"date"},"estimated_cost_wasted":{"type":["number","null"]}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id/image","method":"POST","description":"Upload or replace a recipe hero image. Accepts EITHER a publicly accessible image_url (preferred — avoids base64 size limits) OR base64-encoded image data. The image is stored in a private Supabase Storage bucket (recipe-images). The response includes storage_path (relative path within the bucket) and image_signed_url (a time-limited URL valid for 1 hour). Use image_signed_url for display — do not cache it beyond expiry.","inputSchema":{"type":"object","required":[],"properties":{"image_url":{"type":"string","description":"Publicly accessible URL of the image to upload. The server fetches it directly — avoids base64 size limits. Preferred over the image parameter. Supports JPEG, PNG, and WebP (max 10 MB)."},"image":{"type":"string","description":"Base64-encoded image data. May include a data URI prefix (e.g. 'data:image/jpeg;base64,...') — the prefix will be stripped automatically. Max 5 MB decoded. Use image_url instead when possible."},"content_type":{"type":"string","description":"MIME type of the image. Only needed with base64 image parameter. Supported values: 'image/jpeg', 'image/png', 'image/webp'. Defaults to 'image/jpeg'. Ignored when using image_url (detected automatically)."}}},"outputSchema":{"type":"object","properties":{"storage_path":{"type":"string","description":"Relative path within the recipe-images private bucket (recipes/{recipe_id}/{uuid}.{ext}; contains no user id)."},"image_signed_url":{"type":["string","null"],"description":"Time-limited signed URL for displaying the image (valid for 1 hour). Do not cache beyond expiry."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/recipes/:recipe_id/image","method":"DELETE","description":"Remove a recipe's hero image from the private recipe-images bucket and clear the image_url.","inputSchema":{"type":"object","properties":{}},"outputSchema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation message."}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/today-snapshot","method":"GET","description":"Return today's nutrition snapshot for planning conversations: the caller's daily calorie/protein/goal-weight targets, every meal already logged today (with calories + protein resolved from the linked recipe when available, the per-meal estimate when not, or null when neither source exists), every meal still planned but not yet eaten, consumed-so-far and projected end-of-day totals (projected includes planned meals — this is critical so the model can advise on lunch knowing what dinner already looks like), an unknown_meal_count for meals with no macro data, and the caller's weight summary (latest entry, 7-day avg, 30-day delta, trend). Designed to be read before advising on what to eat next — e.g. 'what should I have for lunch?' or 'how many calories do I have left today?'. family_member_id defaults to the caller's own family member; date defaults to today in Atlantic/Reykjavik. Goals and weight_summary are zero/null when querying a different family member, since those are owner-private.","inputSchema":{"type":"object","properties":{"family_member_id":{"type":"string","description":"Optional UUID of a family member in the caller's family. Defaults to the caller's own family_members row. When set to a different member, goals and weight_summary will be null/empty (those are owner-private)."},"date":{"type":"string","description":"Optional date in YYYY-MM-DD. Defaults to today in the user's timezone (Atlantic/Reykjavik)."}}},"outputSchema":{"type":"object","properties":{"family_member_id":{"type":"string"},"date":{"type":"string"},"goals":{"oneOf":[{"type":"null"},{"type":"object","properties":{"daily_calorie_target":{"type":["integer","null"]},"daily_protein_g_target":{"type":["integer","null"]},"goal_weight_kg":{"type":["number","null"]}}}],"description":"Caller's nutrition + goal-weight targets, or null when no row exists / when querying another family member."},"logged_meals":{"type":"array","description":"Meals already logged for this family member today.","items":{"type":"object","properties":{"id":{"type":"string"},"meal_plan_id":{"type":["string","null"]},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"]},"meal_source":{"type":["string","null"]},"description":{"type":["string","null"]},"location":{"type":["string","null"]},"calories":{"type":["number","null"]},"protein_g":{"type":["number","null"]},"macro_source":{"type":"string","enum":["recipe","estimate","unknown"],"description":"Where the macros came from: 'recipe' = summed from linked recipes' per-serving nutrition, 'estimate' = per-meal estimated_calories/estimated_protein_g, 'unknown' = no macro data available."}}}},"planned_meals":{"type":"array","description":"Meal plans for today NOT yet logged as person_meals for this family member. Use these to project what the rest of the day looks like before advising on the next meal.","items":{"type":"object","properties":{"id":{"type":"null"},"meal_plan_id":{"type":"string"},"meal_type":{"type":"string"},"meal_source":{"type":["string","null"]},"description":{"type":["string","null"]},"location":{"type":["string","null"]},"calories":{"type":["number","null"]},"protein_g":{"type":["number","null"]},"macro_source":{"type":"string","enum":["recipe","estimate","unknown"]}}}},"totals":{"type":"object","properties":{"consumed":{"type":"object","description":"Sum of macros from logged meals only.","properties":{"calories":{"type":"number"},"protein_g":{"type":"number"}}},"projected":{"type":"object","description":"Consumed + planned macros — what the full day will look like if all planned meals are eaten.","properties":{"calories":{"type":"number"},"protein_g":{"type":"number"}}},"unknown_meal_count":{"type":"integer","description":"Count of meals (logged + planned) with macro_source='unknown'."}}},"weight_summary":{"type":"object","description":"Same shape as /api/v1/health/weight-log/summary. Latest entry, 7-day avg, 30-day delta, trend direction, 90-day entry count. Empty for non-caller targets.","properties":{"latest":{"description":"Most recent weight log entry, or null if no entries exist.","oneOf":[{"type":"null"},{"type":"object","properties":{"id":{"type":"string"},"recorded_at":{"type":"string"},"weight_kg":{"type":"number"},"note":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}]},"seven_day_avg":{"type":["number","null"]},"thirty_day_delta_kg":{"type":["number","null"]},"trend":{"type":["string","null"],"enum":["down","flat","up",null]},"entry_count":{"type":"integer"}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/kronan/links","method":"GET","description":"List Krónan product↔ingredient mapping links for the family. Filter with ?status=suggested|confirmed|rejected. Suggested links are candidates awaiting review; only confirmed links produce price observations. Multiple confirmed SKUs can map to one ingredient; is_primary marks the one whose price drives the ingredient's reference_price.","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["suggested","confirmed","rejected"],"description":"Filter links by status."}}},"outputSchema":{"type":"object","properties":{"links":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"ingredient_id":{"type":"string"},"kronan_sku":{"type":"string"},"product_name":{"type":"string"},"package_quantity":{"type":["number","null"]},"package_unit":{"type":["string","null"]},"status":{"type":"string","enum":["suggested","confirmed","rejected"]},"is_primary":{"type":"boolean","description":"True for the one confirmed link per ingredient whose price drives reference_price."},"suggestion_score":{"type":["number","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/kronan/links","method":"POST","description":"Manually link a Krónan SKU to an ingredient (created as confirmed). Requires ingredient_id, kronan_sku, product_name; optional package_quantity + package_unit (needed before price observations flow). The first confirmed link for an ingredient is auto-marked is_primary (drives reference_price); subsequent links are added as non-primary.","inputSchema":{"type":"object","required":["ingredient_id","kronan_sku","product_name"],"properties":{"ingredient_id":{"type":"string","description":"UUID of the public.ingredients row to link."},"kronan_sku":{"type":"string","description":"Krónan product SKU."},"product_name":{"type":"string","description":"Krónan product display name."},"package_quantity":{"type":"number","description":"Package size, e.g. 1 for 1L milk."},"package_unit":{"type":"string","description":"Unit of measure (l, kg, g, ml, stk, etc.)."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"family_id":{"type":"string"},"ingredient_id":{"type":"string"},"kronan_sku":{"type":"string"},"product_name":{"type":"string"},"package_quantity":{"type":["number","null"]},"package_unit":{"type":["string","null"]},"status":{"type":"string","enum":["suggested","confirmed","rejected"]},"is_primary":{"type":"boolean","description":"True when this is the first confirmed link for the ingredient (auto-marked primary)."},"suggestion_score":{"type":["number","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/kronan/links/:link_id","method":"PATCH","description":"Update a Krónan mapping link: confirm/reject a suggestion (status), set package_quantity/package_unit, or set is_primary. Setting is_primary=true (only allowed on confirmed links) atomically demotes the ingredient's previous primary; rejecting a link clears its is_primary.","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["suggested","confirmed","rejected"],"description":"New status. Use 'confirmed' to accept a suggestion, 'rejected' to dismiss it."},"package_quantity":{"type":"number","description":"Package size, e.g. 1 for 1L milk."},"package_unit":{"type":"string","description":"Unit of measure (l, kg, g, ml, stk, etc.)."},"is_primary":{"type":"boolean","description":"Set true to make this the ingredient's primary SKU (price driver). Only confirmed links may be primary; the previous primary is demoted automatically."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"family_id":{"type":"string"},"ingredient_id":{"type":"string"},"kronan_sku":{"type":"string"},"product_name":{"type":"string"},"package_quantity":{"type":["number","null"]},"package_unit":{"type":["string","null"]},"status":{"type":"string","enum":["suggested","confirmed","rejected"]},"is_primary":{"type":"boolean","description":"Whether this link is the ingredient's primary (reference_price) SKU."},"suggestion_score":{"type":["number","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/kronan/products/search","method":"GET","description":"Search the Krónan product catalog (live API, family token). Query param q. Returns products with sku, name, price (ISK), discountedPrice, onSale, baseComparisonUnit — for finding SKUs to link to ingredients.","inputSchema":{"type":"object","required":["q"],"properties":{"q":{"type":"string","description":"Search term, e.g. 'mjólk'"},"page":{"type":"integer","minimum":1,"description":"Result page, default 1"},"pageSize":{"type":"integer","minimum":1,"description":"Results per page, default 20"}}},"outputSchema":{"type":"object","properties":{"products":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"name":{"type":"string"},"price":{"type":"number"},"discountedPrice":{"type":"number"},"onSale":{"type":"boolean"},"chargedByWeight":{"type":"boolean"},"pricePerKilo":{"type":["number","null"]},"baseComparisonUnit":{"type":["string","null"]},"brand":{"type":["string","null"]},"categoryPath":{"type":["string","null"]},"thumbnail":{"type":["string","null"]}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":true},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/kronan/links/suggest","method":"POST","description":"Run the Krónan mapping suggestion engine: scores the family's purchase history against unmapped ingredients (name similarity + purchase frequency, no AI calls) and writes 'suggested' links for review. Idempotent; never modifies confirmed or rejected links.","outputSchema":{"type":"object","properties":{"candidates":{"type":"number"},"unmappedIngredients":{"type":"number"},"suggestionsWritten":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/kronan/prices/refresh","method":"POST","description":"Manually run the Krónan price sync for your family (family admin): batch-fetches live prices for confirmed SKU links and writes change-only observations to ingredient price tracking. Normally runs nightly via cron.","outputSchema":{"type":"object","properties":{"familiesProcessed":{"type":"number"},"skusFetched":{"type":"number"},"observationsWritten":{"type":"number"},"skippedNoPackage":{"type":"number"},"failures":{"type":"array","items":{"type":"string"}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/storage-locations","method":"GET","description":"List the family's food storage locations (freezers, fridge, pantry). The first call by a parent creates the two default freezers: 'Garage chest freezer' (default) and 'Fridge freezer'.","inputSchema":{"type":"object","properties":{"kind":{"type":"string","enum":["freezer","fridge","pantry"],"description":"Filter by kind."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/storage-locations","method":"POST","description":"Parents only. Add a storage location, e.g. a second chest freezer.","inputSchema":{"type":"object","required":["name","kind"],"properties":{"name":{"type":"string","description":"Full name, e.g. 'Basement freezer'."},"short_name":{"type":"string","description":"Name used in reminders, e.g. 'basement freezer'. Defaults to name."},"kind":{"type":"string","enum":["freezer","fridge","pantry"]},"sort_order":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/storage-locations/:location_id","method":"PATCH","description":"Parents only. Rename, reorder, archive, or make a location the default for its kind.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"short_name":{"type":"string"},"sort_order":{"type":"number"},"is_default":{"type":"boolean","description":"true makes this the default location of its kind."},"archived":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/inventory/stock","method":"GET","description":"What's in the freezers, oldest first. Each entry has grams_remaining, grams_reserved (earmarked for planned meals), grams_available, packs_display like '2 packs (1,800 g)', frozen_on, best_before, age_days and warnings (use_soon, past_best_before, thawing). Also returns per-ingredient totals per location. Use for 'what's in the freezer?' or before planning meals from stock.","inputSchema":{"type":"object","properties":{"location_id":{"type":"string"},"ingredient_id":{"type":"string"},"is_protein":{"type":"boolean","description":"Only protein (true) or only non-protein (false)."},"kind":{"type":"string","enum":["freezer","fridge","pantry","all"],"description":"Default: freezer."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/inventory/entries/:entry_id","method":"GET","description":"One freezer entry with its full event history and reservations.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/inventory/add","method":"POST","description":"Parents only. Put food in the freezer. Give grams, or packs (uses pack_grams or the ingredient's default pack size), or pieces (uses grams_per_piece). Location defaults to the ingredient's default location, else the default freezer. An ingredient without freezer settings gets category defaults (inferred from the name, or pass freezer_category) and the response says profile_created: true so you can tell the user once. For a stocktake pass items: [...] (up to 50).","inputSchema":{"type":"object","properties":{"ingredient_id":{"type":"string","description":"Ingredient id (use this or name)."},"name":{"type":"string","description":"Ingredient name; matched case-insensitively within the family, created if new."},"grams":{"type":"number"},"packs":{"type":"number"},"pieces":{"type":"number"},"pack_grams":{"type":"number","description":"Pack size for this batch, e.g. 900."},"location_id":{"type":"string"},"frozen_on":{"type":"string","format":"date","description":"Date frozen (YYYY-MM-DD). Default today."},"best_before":{"type":"string","format":"date","description":"Override best-before. Default frozen_on + the profile's max freezer months."},"freezer_category":{"type":"string","enum":["poultry","mince","fish","red_meat","whole_or_roast","prepared","veg","bread","dessert","other"],"description":"Needed only when the ingredient is new and its name doesn't reveal the category."},"display_name":{"type":"string","description":"Label as bought, e.g. the Krónan product name."},"notes":{"type":"string"},"items":{"type":"array","description":"Batch add. Each item takes the same fields as a single add.","items":{"type":"object"}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/inventory/take-out","method":"POST","description":"Parents only. Take food out of the freezer. Picks the oldest entry of the ingredient unless entry_id is given. If that entry is reserved for a planned meal, this marks it as thawing for that meal (like tapping Done on the thaw reminder) and the grams stay on the entry until the meal is cooked. Otherwise the grams leave the freezer: default one pack, or pass grams/packs. Pass thaw: false to remove grams even when reserved.","inputSchema":{"type":"object","properties":{"entry_id":{"type":"string"},"ingredient_id":{"type":"string","description":"Ingredient id (use this or name)."},"name":{"type":"string","description":"Ingredient name; matched case-insensitively within the family, created if new."},"grams":{"type":"number"},"packs":{"type":"number","description":"Default 1."},"meal_plan_id":{"type":"string","description":"Planned meal this is for."},"thaw":{"type":"boolean","description":"Default true."},"reason":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/inventory/return","method":"POST","description":"Parents only. Put food back that was taken out but never thawed (e.g. by mistake, or part of a free-flow bag). Refused once an entry has started thawing: thawed raw food must not be refrozen.","inputSchema":{"type":"object","required":["entry_id","grams"],"properties":{"entry_id":{"type":"string"},"grams":{"type":"number","description":"Amount being put back."},"note":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/inventory/discard","method":"POST","description":"Parents only. Throw out a freezer entry (default all of it) or part of it.","inputSchema":{"type":"object","required":["entry_id"],"properties":{"entry_id":{"type":"string"},"grams":{"type":"number","description":"Partial amount; omit to discard everything left."},"reason":{"type":"string","enum":["freezer_burn","expired","thawed_too_long","other"]},"note":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/inventory/move","method":"POST","description":"Parents only. Move a freezer entry to another location.","inputSchema":{"type":"object","required":["entry_id","location_id"],"properties":{"entry_id":{"type":"string"},"location_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/inventory/adjust","method":"POST","description":"Parents only. Stocktake correction: set an entry's remaining grams to what's actually there.","inputSchema":{"type":"object","required":["entry_id","grams_remaining"],"properties":{"entry_id":{"type":"string"},"grams_remaining":{"type":"number"},"note":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/freezer-profiles","method":"GET","description":"Freezer settings per ingredient with category defaults filled in: freezer_category, is_protein, thaw_lead_days, thaw_method, default_pack_grams, grams_per_piece, is_free_flow, max_freezer_months, default_location_id. source is 'category_default' until someone edits it.","inputSchema":{"type":"object","properties":{"ingredient_id":{"type":"string"},"source":{"type":"string","enum":["category_default","user"]}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/freezer-profiles/update","method":"POST","description":"Parents only. Set an ingredient's freezer settings once, e.g. 'chicken thighs take 1 day to thaw, come in 700 g packs'. null resets a field to the category default. Affects future reservations only.","inputSchema":{"type":"object","properties":{"ingredient_id":{"type":"string","description":"Ingredient id (use this or name)."},"name":{"type":"string","description":"Ingredient name; matched case-insensitively within the family, created if new."},"freezer_category":{"type":"string","enum":["poultry","mince","fish","red_meat","whole_or_roast","prepared","veg","bread","dessert","other"]},"is_protein":{"type":["boolean","null"],"description":"Protein items get reservations and thaw reminders."},"thaw_lead_days":{"type":["number","null"],"description":"0 = take out the morning of the meal."},"thaw_method":{"type":["string","null"],"enum":["fridge","cold_water","from_frozen",null]},"quick_thaw_note":{"type":["string","null"]},"default_pack_grams":{"type":["number","null"]},"grams_per_piece":{"type":["number","null"],"description":"Lets recipes in 'stk' convert to grams."},"is_free_flow":{"type":"boolean","description":"Loose items that can be portioned frozen (strips, prawns)."},"max_freezer_months":{"type":["number","null"]},"default_location_id":{"type":["string","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/freezer-category-defaults","method":"GET","description":"Default freezer settings per category (thaw lead days, method, shelf life, pack size, quick-thaw advice).","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/inventory/reservations","method":"GET","description":"Freezer stock earmarked for planned meals in a date range (default: open reservations for the next 7 days).","inputSchema":{"type":"object","properties":{"from":{"type":"string","format":"date","description":"Start date. Default today."},"to":{"type":"string","format":"date","description":"End date. Default from + 7 days."},"status":{"type":"string","enum":["active","thawing","consumed","released"]}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/inventory/reserve-plan","method":"POST","description":"Parents only. Reserve freezer protein for planned home-cooked meals in a date range, oldest stock first (idempotent; call after planning a week). Returns reserved/updated/released reservations, shortfalls (protein to buy), cant_tell (recipe lines in units that don't convert to grams) and warnings (food already thawing whose meal moved or disappeared).","inputSchema":{"type":"object","properties":{"start_date":{"type":"string","format":"date","description":"Default today."},"end_date":{"type":"string","format":"date","description":"Default start_date + 3 days. Max 31-day range."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/inventory/release","method":"POST","description":"Parents only. Un-reserve a planned meal's freezer stock (e.g. 'we're eating out on Tuesday'). Does not delete the plan. Food that is already thawing is reported, not returned to the freezer.","inputSchema":{"type":"object","required":["meal_plan_id"],"properties":{"meal_plan_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/inventory/consume-meal","method":"POST","description":"Parents only. The meal was cooked: use up its reserved freezer stock.","inputSchema":{"type":"object","required":["meal_plan_id"],"properties":{"meal_plan_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/meals/thaw-tasks","method":"GET","description":"What to take out of the freezer tonight, with the exact text of the evening thaw notification: due (take out tonight, or tomorrow morning for fish), overdue (missed thaws with a quick-thaw or swap suggestion), shortfalls, cant_tell and warnings for the next 3 days, plus title/body and confirmableReservationIds for meals_thaw_tasks_confirm.","inputSchema":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"Evening to plan for. Default today."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"meals"},{"path":"/api/v1/meals/thaw-tasks/confirm","method":"POST","description":"Parents only. 'Done': the food is out of the freezer. Marks the reservations and their entries as thawing. Pass reservation_ids, or a date to confirm everything due that evening. Idempotent.","inputSchema":{"type":"object","properties":{"reservation_ids":{"type":"array","items":{"type":"string"}},"date":{"type":"string","format":"date","description":"Confirm everything due on this evening. Default today."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"meals"},{"path":"/api/v1/fitness/programs/create","method":"POST","description":"Create a new workout program (template) with a list of exercises and their targets. Use when a user describes a training program they want to follow — for example 'create a push/pull/legs split' or 'set up a 5x5 strength program'. Each exercise references the exercise catalog by ID and can include target sets, rep ranges, RPE, rest times, and superset grouping.","inputSchema":{"type":"object","required":["name","exercises"],"properties":{"name":{"type":"string","description":"Program name (e.g. 'Push/Pull/Legs — Push Day')."},"description":{"type":"string","description":"Optional description of the program's goals or structure."},"target_muscles":{"type":"array","items":{"type":"string","enum":["chest","back","shoulders","biceps","triceps","forearms","quadriceps","hamstrings","glutes","calves","core","full_body"]},"description":"Primary muscle groups targeted by this program."},"estimated_duration_minutes":{"type":"number","description":"Estimated workout duration in minutes."},"exercises":{"type":"array","minItems":1,"items":{"type":"object","required":["exercise_id"],"properties":{"exercise_id":{"type":"string","description":"UUID of the exercise from the exercise catalog."},"sort_order":{"type":"number","description":"Position in the workout (0-indexed). Defaults to array index."},"target_sets":{"type":"number","description":"Number of target sets."},"target_reps_min":{"type":"number","description":"Minimum target reps per set."},"target_reps_max":{"type":"number","description":"Maximum target reps per set."},"target_rpe":{"type":"number","description":"Target Rate of Perceived Exertion (1-10)."},"target_duration_seconds":{"type":"number","description":"Target duration for timed exercises."},"rest_seconds":{"type":"number","description":"Rest time between sets in seconds."},"notes":{"type":"string","description":"Exercise-specific notes or cues."},"superset_group":{"type":"number","description":"Group number for supersets. Exercises with the same group are performed back-to-back."},"format":{"type":"string","enum":["straight","superset","emom","complex"],"description":"Exercise format. Defaults to 'straight'. 'complex' (kettlebell/barbell complexes) is a grouped format like 'superset' — group via superset_group + sort_order."}}},"description":"Ordered list of exercises with their targets."}}},"outputSchema":{"type":"object","properties":{"program_id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"exercise_count":{"type":"number"},"exercises":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Template exercise ID."},"exercise_id":{"type":"string"},"exercise_name":{"type":"string"},"sort_order":{"type":"number"},"target_sets":{"type":["number","null"]},"target_reps_min":{"type":["number","null"]},"target_reps_max":{"type":["number","null"]},"format":{"type":"string","enum":["straight","superset","emom","complex"]}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/programs","method":"GET","description":"List all workout programs (templates) for the user. Use when a user asks 'What programs do I have?' or wants to browse their saved routines. Supports text search on program name, filtering by favorites and archived status, and pagination. Returns programs sorted by favorites first, then most recently used. Each program includes an exercise_count for quick overview.","inputSchema":{"type":"object","properties":{"search":{"type":"string","description":"Text search on program name (case-insensitive partial match)."},"is_favorite":{"type":"boolean","description":"Filter to favorite programs only."},"is_archived":{"type":"boolean","description":"Filter by archived status. Default: false (only active programs)."},"limit":{"type":"number","description":"Max results (1-100, default 50)."},"offset":{"type":"number","description":"Pagination offset (default 0)."}}},"outputSchema":{"type":"object","properties":{"programs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"target_muscles":{"type":"array","items":{"type":"string"}},"estimated_duration_minutes":{"type":["number","null"]},"is_favorite":{"type":"boolean"},"is_archived":{"type":"boolean"},"last_used_at":{"type":["string","null"]},"use_count":{"type":"number"},"exercise_count":{"type":"number"}}}},"count":{"type":"number","description":"Total number of matching programs."},"has_more":{"type":"boolean","description":"Whether there are more programs beyond this page."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/programs/:id","method":"GET","description":"Get full details for a single workout program including all exercises with their targets and exercise catalog metadata (names, muscle groups). Use when a user asks about a specific program — e.g. 'Show me my Push Day program' or when you need the exercise list to suggest modifications.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"target_muscles":{"type":"array","items":{"type":"string"}},"estimated_duration_minutes":{"type":["number","null"]},"is_favorite":{"type":"boolean"},"is_archived":{"type":"boolean"},"last_used_at":{"type":["string","null"]},"use_count":{"type":"number"},"exercises":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Template exercise ID."},"exercise_id":{"type":"string"},"exercise_name":{"type":"string"},"primary_muscle":{"type":"string"},"sort_order":{"type":"number"},"target_sets":{"type":["number","null"]},"target_reps_min":{"type":["number","null"]},"target_reps_max":{"type":["number","null"]},"target_rpe":{"type":["number","null"]},"target_duration_seconds":{"type":["number","null"]},"rest_seconds":{"type":["number","null"]},"notes":{"type":["string","null"]},"superset_group":{"type":["number","null"]},"format":{"type":"string","enum":["straight","superset","emom","complex"]}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/programs/:id","method":"PATCH","description":"Update a program's metadata — name, description, target muscles, estimated duration, favorite status, or archived status. Use when a user wants to rename a program, mark it as a favorite, or update its description. Does not modify the exercise list (use POST programs/:id/exercises for that).","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Program name."},"description":{"type":["string","null"],"description":"Program description."},"target_muscles":{"type":"array","items":{"type":"string","enum":["chest","back","shoulders","biceps","triceps","forearms","quadriceps","hamstrings","glutes","calves","core","full_body"]},"description":"Primary muscle groups targeted."},"estimated_duration_minutes":{"type":["number","null"],"description":"Estimated duration in minutes."},"is_favorite":{"type":"boolean","description":"Mark as favorite."},"is_archived":{"type":"boolean","description":"Archive or unarchive."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"target_muscles":{"type":"array","items":{"type":"string"}},"estimated_duration_minutes":{"type":["number","null"]},"is_favorite":{"type":"boolean"},"is_archived":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/programs/:id/exercises","method":"POST","description":"Replace all exercises in a program with a new list. Use when a user wants to restructure a program — e.g. 'swap the bench press for incline press' or 'reorder the exercises in my leg day'. This is a full replacement: all existing exercises are removed and the new list is inserted. Send the complete desired exercise list.","inputSchema":{"type":"object","required":["exercises"],"properties":{"exercises":{"type":"array","minItems":1,"items":{"type":"object","required":["exercise_id"],"properties":{"exercise_id":{"type":"string","description":"UUID of the exercise from the exercise catalog."},"sort_order":{"type":"number","description":"Position in the workout (0-indexed). Defaults to array index."},"target_sets":{"type":"number","description":"Number of target sets."},"target_reps_min":{"type":"number","description":"Minimum target reps per set."},"target_reps_max":{"type":"number","description":"Maximum target reps per set."},"target_rpe":{"type":"number","description":"Target Rate of Perceived Exertion (1-10)."},"target_duration_seconds":{"type":"number","description":"Target duration for timed exercises."},"rest_seconds":{"type":"number","description":"Rest time between sets in seconds."},"notes":{"type":"string","description":"Exercise-specific notes or cues."},"superset_group":{"type":"number","description":"Group number for supersets."},"format":{"type":"string","enum":["straight","superset","emom","complex"],"description":"Exercise format. Defaults to 'straight'. 'complex' is a grouped format like 'superset'."}}},"description":"Complete ordered list of exercises to replace the current list."}}},"outputSchema":{"type":"object","properties":{"program_id":{"type":"string"},"exercise_count":{"type":"number"},"exercises":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"exercise_id":{"type":"string"},"exercise_name":{"type":"string"},"primary_muscle":{"type":"string"},"sort_order":{"type":"number"},"target_sets":{"type":["number","null"]},"target_reps_min":{"type":["number","null"]},"target_reps_max":{"type":["number","null"]},"format":{"type":"string","enum":["straight","superset","emom","complex"]}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/programs/:id","method":"DELETE","description":"Archive a program (soft delete). Use when a user says 'Delete my old program' or 'I don't use this routine anymore'. Sets is_archived to true rather than hard-deleting, preserving workout history. Fails if the program has active or scheduled workouts — those must be completed or cancelled first.","outputSchema":{"type":"object","properties":{"archived":{"type":"boolean"},"program_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/workouts/upcoming","method":"GET","description":"Get upcoming scheduled workouts with their planned exercises and last-performance context. Use when a user asks 'What's my next workout?' or wants to prepare for an upcoming session. Returns both explicitly scheduled workouts and recurring schedule occurrences. Each exercise includes the target sets/reps from the template and what the user actually did last time — useful for suggesting weights or noticing progression.","inputSchema":{"type":"object","properties":{"limit":{"type":"number","description":"Maximum number of upcoming workouts to return (default: 7, max: 30)."},"program_id":{"type":"string","description":"Filter to workouts from a specific program/template UUID."}}},"outputSchema":{"type":"object","properties":{"workouts":{"type":"array","items":{"type":"object","properties":{"id":{"type":["string","null"],"description":"Workout ID (null for recurring schedule projections)."},"name":{"type":"string"},"scheduled_for":{"type":["string","null"],"format":"date"},"source":{"type":"string","enum":["scheduled","recurring"]},"template_id":{"type":["string","null"]},"template_name":{"type":["string","null"]},"exercises":{"type":"array","items":{"type":"object","properties":{"exercise_id":{"type":"string"},"exercise_name":{"type":"string"},"primary_muscle":{"type":"string"},"exercise_type":{"type":"string"},"target_sets":{"type":["number","null"]},"target_reps_min":{"type":["number","null"]},"target_reps_max":{"type":["number","null"]},"target_rpe":{"type":["number","null"]},"rest_seconds":{"type":["number","null"]},"last_performance":{"type":["object","null"],"description":"What the user did last time for this exercise. For cable sets, plate counts and resolved actual_kg are returned alongside the raw weight_kg field.","properties":{"date":{"type":"string","format":"date"},"sets":{"type":"array","items":{"type":"object","properties":{"set_number":{"type":"number"},"weight_kg":{"type":["number","null"],"description":"Loaded weight for free-weight sets. NULL for cable sets — see plates_count / actual_kg."},"reps":{"type":["number","null"]},"rpe":{"type":["number","null"]},"plates_count":{"type":["number","null"],"description":"Bilateral cable plate count."},"plates_left":{"type":["number","null"],"description":"Unilateral cable: left side plate count."},"plates_right":{"type":["number","null"],"description":"Unilateral cable: right side plate count."},"equipment_item_id":{"type":["string","null"],"description":"Cable machine UUID."},"pulley_name":{"type":["string","null"],"description":"Pulley position name on the machine."},"actual_kg":{"type":["number","null"],"description":"Server-resolved kg for bilateral cable sets."},"actual_kg_left":{"type":["number","null"],"description":"Server-resolved kg for the left side of unilateral cable sets."},"actual_kg_right":{"type":["number","null"],"description":"Server-resolved kg for the right side of unilateral cable sets."}}}}}}}}},"recommendations":{"type":"array","description":"Pending exercise-level coach recommendations relevant to this workout/program. Advisory only; planned targets are unchanged.","items":{"type":"object","properties":{"id":{"type":"string"},"recommendation_type":{"type":"string","enum":["increase_load","increase_reps","maintain","reduce_load","reduce_volume","increase_volume","change_exercise","adjust_rpe","technique_focus","recovery","other"]},"status":{"type":"string","enum":["pending","accepted","dismissed","superseded"]},"program_id":{"type":["string","null"]},"upcoming_workout_id":{"type":["string","null"]},"source_workout_id":{"type":"string"},"source_workout_exercise_id":{"type":["string","null"]},"exercise_id":{"type":"string"},"target":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"target_sets":{"type":"integer","minimum":1,"maximum":100},"target_reps":{"type":"integer","minimum":1,"maximum":1000},"target_rep_min":{"type":"integer","minimum":1,"maximum":1000},"target_rep_max":{"type":"integer","minimum":1,"maximum":1000},"target_weight_kg":{"type":"number","minimum":0,"maximum":2000},"target_rpe":{"type":"number","minimum":1,"maximum":10},"target_rpe_min":{"type":"number","minimum":1,"maximum":10},"target_rpe_max":{"type":"number","minimum":1,"maximum":10},"target_rest_seconds":{"type":"integer","minimum":0,"maximum":86400},"target_duration_seconds":{"type":"integer","minimum":1,"maximum":86400},"instruction":{"type":"string","maxLength":500}}},"reasoning":{"type":"string"},"created_by":{"type":"string","enum":["external_coach","user","system"]},"last_action_by":{"type":["string","null"],"enum":["external_coach","user","system",null]},"supersedes_id":{"type":["string","null"]},"superseded_by_id":{"type":["string","null"]},"accepted_at":{"type":["string","null"]},"dismissed_at":{"type":["string","null"]},"superseded_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/coach/recommendations","method":"GET","description":"List persisted exercise-level coaching recommendations. Pending recommendations are returned by default. Filter by program, upcoming workout, source completed workout, exercise, or lifecycle status. Recommendations are advisory and never rewrite workout templates or logs.","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["pending","accepted","dismissed","superseded","all"],"description":"Lifecycle filter. Defaults to pending; use all to include history."},"program_id":{"type":"string"},"workout_id":{"type":"string","description":"Filter by the target upcoming workout UUID."},"source_workout_id":{"type":"string"},"exercise_id":{"type":"string"},"limit":{"type":"integer","minimum":1,"maximum":100}}},"outputSchema":{"type":"object","properties":{"recommendations":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"recommendation_type":{"type":"string","enum":["increase_load","increase_reps","maintain","reduce_load","reduce_volume","increase_volume","change_exercise","adjust_rpe","technique_focus","recovery","other"]},"status":{"type":"string","enum":["pending","accepted","dismissed","superseded"]},"program_id":{"type":["string","null"]},"upcoming_workout_id":{"type":["string","null"]},"source_workout_id":{"type":"string"},"source_workout_exercise_id":{"type":["string","null"]},"exercise_id":{"type":"string"},"target":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"target_sets":{"type":"integer","minimum":1,"maximum":100},"target_reps":{"type":"integer","minimum":1,"maximum":1000},"target_rep_min":{"type":"integer","minimum":1,"maximum":1000},"target_rep_max":{"type":"integer","minimum":1,"maximum":1000},"target_weight_kg":{"type":"number","minimum":0,"maximum":2000},"target_rpe":{"type":"number","minimum":1,"maximum":10},"target_rpe_min":{"type":"number","minimum":1,"maximum":10},"target_rpe_max":{"type":"number","minimum":1,"maximum":10},"target_rest_seconds":{"type":"integer","minimum":0,"maximum":86400},"target_duration_seconds":{"type":"integer","minimum":1,"maximum":86400},"instruction":{"type":"string","maxLength":500}}},"reasoning":{"type":"string"},"created_by":{"type":"string","enum":["external_coach","user","system"]},"last_action_by":{"type":["string","null"],"enum":["external_coach","user","system",null]},"supersedes_id":{"type":["string","null"]},"superseded_by_id":{"type":["string","null"]},"accepted_at":{"type":["string","null"]},"dismissed_at":{"type":["string","null"]},"superseded_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/coach/recommendations","method":"POST","description":"Persist an external coach's recommendation for one exercise, grounded in a completed source workout and scoped to a program and/or upcoming workout. Requires an idempotency key. A newer pending recommendation for the same scope supersedes the old one atomically; no workout target or log is modified.","inputSchema":{"type":"object","required":["recommendation_type","source_workout_id","exercise_id","target","reasoning","idempotency_key"],"properties":{"recommendation_type":{"type":"string","enum":["increase_load","increase_reps","maintain","reduce_load","reduce_volume","increase_volume","change_exercise","adjust_rpe","technique_focus","recovery","other"]},"program_id":{"type":["string","null"],"description":"Owned workout program UUID. program_id or upcoming_workout_id is required."},"upcoming_workout_id":{"type":["string","null"],"description":"Owned scheduled workout instance UUID. program_id or upcoming_workout_id is required."},"source_workout_id":{"type":"string","description":"Owned completed workout that provides the recommendation evidence."},"source_workout_exercise_id":{"type":["string","null"],"description":"Optional workout-exercise row UUID; must belong to the source workout and match exercise_id."},"exercise_id":{"type":"string"},"target":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"target_sets":{"type":"integer","minimum":1,"maximum":100},"target_reps":{"type":"integer","minimum":1,"maximum":1000},"target_rep_min":{"type":"integer","minimum":1,"maximum":1000},"target_rep_max":{"type":"integer","minimum":1,"maximum":1000},"target_weight_kg":{"type":"number","minimum":0,"maximum":2000},"target_rpe":{"type":"number","minimum":1,"maximum":10},"target_rpe_min":{"type":"number","minimum":1,"maximum":10},"target_rpe_max":{"type":"number","minimum":1,"maximum":10},"target_rest_seconds":{"type":"integer","minimum":0,"maximum":86400},"target_duration_seconds":{"type":"integer","minimum":1,"maximum":86400},"instruction":{"type":"string","maxLength":500}}},"reasoning":{"type":"string","minLength":1,"maxLength":2000},"idempotency_key":{"type":"string","minLength":1,"maxLength":200}}},"outputSchema":{"type":"object","properties":{"recommendation":{"type":"object","properties":{"id":{"type":"string"},"recommendation_type":{"type":"string","enum":["increase_load","increase_reps","maintain","reduce_load","reduce_volume","increase_volume","change_exercise","adjust_rpe","technique_focus","recovery","other"]},"status":{"type":"string","enum":["pending","accepted","dismissed","superseded"]},"program_id":{"type":["string","null"]},"upcoming_workout_id":{"type":["string","null"]},"source_workout_id":{"type":"string"},"source_workout_exercise_id":{"type":["string","null"]},"exercise_id":{"type":"string"},"target":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"target_sets":{"type":"integer","minimum":1,"maximum":100},"target_reps":{"type":"integer","minimum":1,"maximum":1000},"target_rep_min":{"type":"integer","minimum":1,"maximum":1000},"target_rep_max":{"type":"integer","minimum":1,"maximum":1000},"target_weight_kg":{"type":"number","minimum":0,"maximum":2000},"target_rpe":{"type":"number","minimum":1,"maximum":10},"target_rpe_min":{"type":"number","minimum":1,"maximum":10},"target_rpe_max":{"type":"number","minimum":1,"maximum":10},"target_rest_seconds":{"type":"integer","minimum":0,"maximum":86400},"target_duration_seconds":{"type":"integer","minimum":1,"maximum":86400},"instruction":{"type":"string","maxLength":500}}},"reasoning":{"type":"string"},"created_by":{"type":"string","enum":["external_coach","user","system"]},"last_action_by":{"type":["string","null"],"enum":["external_coach","user","system",null]},"supersedes_id":{"type":["string","null"]},"superseded_by_id":{"type":["string","null"]},"accepted_at":{"type":["string","null"]},"dismissed_at":{"type":["string","null"]},"superseded_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/coach/recommendations/:recommendationId","method":"PATCH","description":"Accept or dismiss a pending coaching recommendation. This records the user's lifecycle decision and audit event only; it never edits workout templates, planned targets, or logged sets.","inputSchema":{"type":"object","required":["recommendationId","status"],"properties":{"recommendationId":{"type":"string","description":"Recommendation UUID (path parameter)."},"status":{"type":"string","enum":["accepted","dismissed"]}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"recommendation_type":{"type":"string","enum":["increase_load","increase_reps","maintain","reduce_load","reduce_volume","increase_volume","change_exercise","adjust_rpe","technique_focus","recovery","other"]},"status":{"type":"string","enum":["pending","accepted","dismissed","superseded"]},"program_id":{"type":["string","null"]},"upcoming_workout_id":{"type":["string","null"]},"source_workout_id":{"type":"string"},"source_workout_exercise_id":{"type":["string","null"]},"exercise_id":{"type":"string"},"target":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"target_sets":{"type":"integer","minimum":1,"maximum":100},"target_reps":{"type":"integer","minimum":1,"maximum":1000},"target_rep_min":{"type":"integer","minimum":1,"maximum":1000},"target_rep_max":{"type":"integer","minimum":1,"maximum":1000},"target_weight_kg":{"type":"number","minimum":0,"maximum":2000},"target_rpe":{"type":"number","minimum":1,"maximum":10},"target_rpe_min":{"type":"number","minimum":1,"maximum":10},"target_rpe_max":{"type":"number","minimum":1,"maximum":10},"target_rest_seconds":{"type":"integer","minimum":0,"maximum":86400},"target_duration_seconds":{"type":"integer","minimum":1,"maximum":86400},"instruction":{"type":"string","maxLength":500}}},"reasoning":{"type":"string"},"created_by":{"type":"string","enum":["external_coach","user","system"]},"last_action_by":{"type":["string","null"],"enum":["external_coach","user","system",null]},"supersedes_id":{"type":["string","null"]},"superseded_by_id":{"type":["string","null"]},"accepted_at":{"type":["string","null"]},"dismissed_at":{"type":["string","null"]},"superseded_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/history/recent","method":"GET","description":"Get recent workout history with full exercise and set details. Use when a user asks about their recent training — for example 'How did my last leg day go?' or 'Show me my workouts this week'. Returns completed workouts in reverse chronological order with every exercise and set logged. Each set includes weight_kg for free-weight sets OR plate count + actual_kg for cable-machine sets (the server resolves cable plates to kg via the equipment's weight_config; clients should display actual_kg for analytics and trend graphs while showing plates_count alongside in cable-set UIs). Unilateral cable sets expose plates_left/plates_right + actual_kg_left/actual_kg_right separately. Supports filtering by exercise and a date range (since + until/end_date) for targeted analysis. Set lean=true for summaries without sets (header, totals and per-exercise set counts) — prefer it for date-range overviews such as 'this week', since the full payload grows quickly. total_volume (kg) and total_sets are computed by the database for completed workouts.","inputSchema":{"type":"object","properties":{"limit":{"type":"number","description":"Number of workouts to return (default: 10, max: 50)."},"offset":{"type":"number","description":"Pagination offset (default: 0)."},"exercise_id":{"type":"string","description":"Filter to workouts containing a specific exercise UUID."},"since":{"type":"string","description":"Only return workouts started on or after this date (YYYY-MM-DD, UTC midnight) or ISO 8601 timestamp."},"until":{"type":"string","description":"Only return workouts started on or before this date (YYYY-MM-DD, inclusive of the whole UTC day) or ISO 8601 timestamp (inclusive). Must not be before since."},"end_date":{"type":"string","description":"Alias for until."},"lean":{"type":"boolean","description":"Return summaries without per-set details: each workout's header and totals plus, per exercise, sets_logged and sets_completed. Default false."}}},"outputSchema":{"type":"object","properties":{"workouts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"started_at":{"type":"string"},"completed_at":{"type":["string","null"]},"duration_seconds":{"type":["number","null"]},"total_volume":{"type":["number","null"]},"total_sets":{"type":["number","null"]},"template_name":{"type":["string","null"]},"exercises":{"type":"array","items":{"type":"object","properties":{"exercise_id":{"type":"string"},"exercise_name":{"type":"string"},"primary_muscle":{"type":"string"},"exercise_type":{"type":"string"},"sort_order":{"type":"number"},"sets_logged":{"type":"number","description":"lean=true only: number of sets logged for this exercise."},"sets_completed":{"type":"number","description":"lean=true only: number of those sets marked completed."},"is_active":{"type":"boolean"},"sets":{"description":"Omitted when lean=true.","type":"array","items":{"type":"object","properties":{"set_number":{"type":"number"},"set_type":{"type":"string"},"weight_kg":{"type":["number","null"],"description":"Loaded weight for free-weight sets. NULL for cable sets — see plates_count / actual_kg instead."},"reps":{"type":["number","null"]},"duration_seconds":{"type":["number","null"]},"distance_meters":{"type":["number","null"]},"rpe":{"type":["number","null"]},"rir":{"type":["number","null"]},"is_completed":{"type":"boolean"},"plates_count":{"type":["number","null"],"description":"Bilateral cable set: number of plates on the pulley. NULL for free-weight sets."},"plates_left":{"type":["number","null"],"description":"Unilateral cable set: plates loaded for the left side. Paired with plates_right."},"plates_right":{"type":["number","null"],"description":"Unilateral cable set: plates loaded for the right side. Paired with plates_left."},"equipment_item_id":{"type":["string","null"],"description":"UUID of the cable machine used (workouts.equipment_items). Set on cable sets only."},"pulley_name":{"type":["string","null"],"description":"Name of the pulley position on the machine (e.g. 'Front Press', 'Butterfly'). Set on cable sets only."},"actual_kg":{"type":["number","null"],"description":"Server-resolved kg for bilateral cable sets — base_weight + plates_count*weight_per_block from the equipment's pulley config. NULL for free-weight sets and unilateral cable sets."},"actual_kg_left":{"type":["number","null"],"description":"Server-resolved kg for the left side of a unilateral cable set. NULL otherwise."},"actual_kg_right":{"type":["number","null"],"description":"Server-resolved kg for the right side of a unilateral cable set. NULL otherwise."}}}}}}}}}},"count":{"type":"number"},"has_more":{"type":"boolean"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/exercises/list","method":"GET","description":"Search and browse the exercise catalog. Use when a user asks about available exercises — e.g. 'What chest exercises do I have?' or 'Show me cable exercises'. Returns both system (built-in) and user-created exercises. Supports text search, muscle group filtering, and equipment type filtering.","inputSchema":{"type":"object","properties":{"search":{"type":"string","description":"Text search on exercise name (case-insensitive partial match)."},"muscle_group":{"type":"string","enum":["chest","back","shoulders","biceps","triceps","forearms","quadriceps","hamstrings","glutes","calves","core","full_body"],"description":"Filter by primary muscle group."},"exercise_type":{"type":"string","enum":["barbell","dumbbell","machine","cable","bodyweight","kettlebell","band","cardio","other"],"description":"Filter by exercise type."},"equipment_type":{"type":"string","enum":["cable_machine","dumbbell","kettlebell","barbell","bodyweight","machine_other","none"],"description":"Filter by equipment type."},"include_archived":{"type":"boolean","description":"Include archived exercises. Default: false."},"limit":{"type":"number","description":"Max results (1-100, default 50)."},"offset":{"type":"number","description":"Pagination offset (default 0)."}}},"outputSchema":{"type":"object","properties":{"exercises":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"primary_muscle":{"type":"string"},"secondary_muscles":{"type":"array","items":{"type":"string"}},"exercise_type":{"type":"string"},"measurement_type":{"type":"string"},"laterality":{"type":"string"},"equipment_type":{"type":["string","null"]},"cable_pulley_position":{"type":["string","null"]},"required_apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]}}},"description":"Apparatus types this exercise can be performed with. OR semantics — at least ONE must be available at the gym."},"is_system":{"type":"boolean"},"is_archived":{"type":"boolean"}}}},"total":{"type":"number"},"limit":{"type":"number"},"offset":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/exercises/:id","method":"GET","description":"Get full details for a single exercise by ID. Use when you need complete exercise information including instructions, video URL, and all metadata.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"primary_muscle":{"type":"string"},"secondary_muscles":{"type":"array","items":{"type":"string"}},"exercise_type":{"type":"string"},"measurement_type":{"type":"string"},"weight_unit":{"type":"string"},"equipment":{"type":["string","null"]},"instructions":{"type":["string","null"]},"video_url":{"type":["string","null"]},"laterality":{"type":"string"},"equipment_type":{"type":["string","null"]},"cable_pulley_position":{"type":["string","null"]},"required_apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]}}},"description":"Apparatus types this exercise can be performed with (OR semantics)."},"is_system":{"type":"boolean"},"is_archived":{"type":"boolean"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/exercises/:id/history","method":"GET","description":"Get per-session workout history for a single exercise, with server-side 1RM estimation (Epley), volume aggregation, and cable plate-to-kg resolution. Includes a personal best aggregate computed across all-time history regardless of the range filter. Use for exercise detail charts (1RM trend, volume trend) and recent sessions list.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Exercise UUID (path parameter)."},"range":{"type":"string","enum":["1m","3m","1y","all"],"description":"Time range filter for sessions. Default: 3m."},"gym_id":{"type":"string","description":"Optional gym UUID to filter sessions by gym."}},"required":["id"]},"outputSchema":{"type":"object","properties":{"exercise_id":{"type":"string"},"range":{"type":"string"},"sessions":{"type":"array","items":{"type":"object","properties":{"workout_id":{"type":"string"},"workout_name":{"type":"string"},"performed_at":{"type":"string"},"gym_id":{"type":["string","null"]},"gym_name":{"type":["string","null"]},"sets":{"type":"array","items":{"type":"object","properties":{"set_number":{"type":"number"},"weight_kg":{"type":"number"},"reps":{"type":"number"},"rpe":{"type":["number","null"]}}}},"top_set":{"type":"object","properties":{"weight_kg":{"type":"number"},"reps":{"type":"number"}}},"total_volume_kg":{"type":"number"},"estimated_1rm_kg":{"type":"number"},"total_sets":{"type":"number"},"total_reps":{"type":"number"}}}},"personal_best":{"type":["object","null"],"properties":{"top_set":{"type":"object","properties":{"weight_kg":{"type":"number"},"reps":{"type":"number"},"performed_at":{"type":"string"}}},"estimated_1rm_kg":{"type":"number"},"max_volume_session_kg":{"type":"number"}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/exercises/:id/context","method":"GET","description":"Get programming and scheduling context for an exercise. Returns which programs include this exercise (with target sets/reps/RPE), the next scheduled workout containing it, the last time it was performed, and whether the user's active gym has the required equipment.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Exercise UUID (path parameter)."}},"required":["id"]},"outputSchema":{"type":"object","properties":{"exercise_id":{"type":"string"},"in_programs":{"type":"array","items":{"type":"object","properties":{"program_id":{"type":"string"},"program_name":{"type":"string"},"target_sets":{"type":["number","null"]},"target_reps":{"type":["string","null"]},"target_rpe":{"type":["number","null"]},"target_weight_kg":{"type":"null"},"notes":{"type":["string","null"]}}}},"next_scheduled":{"type":["object","null"],"properties":{"workout_id":{"type":"string"},"scheduled_for":{"type":"string"},"program_name":{"type":"string"}}},"last_performed":{"type":["object","null"],"properties":{"workout_id":{"type":"string"},"performed_at":{"type":"string"}}},"active_gym_equipment_available":{"type":"boolean"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/exercises/create","method":"POST","description":"Create a custom exercise in the user's exercise catalog. Use when a user wants to add an exercise that doesn't exist in the system catalog — e.g. 'Add a Bulgarian split squat' or 'Create a custom cable fly variation'. The exercise can then be used in programs and workout logging.","inputSchema":{"type":"object","required":["name","primary_muscle","exercise_type"],"properties":{"name":{"type":"string","description":"Exercise name."},"description":{"type":"string","description":"Optional description."},"primary_muscle":{"type":"string","enum":["chest","back","shoulders","biceps","triceps","forearms","quadriceps","hamstrings","glutes","calves","core","full_body"]},"secondary_muscles":{"type":"array","items":{"type":"string"},"description":"Secondary muscle groups."},"exercise_type":{"type":"string","enum":["barbell","dumbbell","machine","cable","bodyweight","kettlebell","band","cardio","other"]},"measurement_type":{"type":"string","enum":["weight_reps","reps_only","duration","distance"],"description":"Default: weight_reps."},"weight_unit":{"type":"string","enum":["kg","lbs","plates"],"description":"Default: kg."},"equipment":{"type":"string","description":"Free-text equipment description."},"instructions":{"type":"string","description":"How to perform the exercise."},"laterality":{"type":"string","enum":["bilateral","unilateral","either"],"description":"Default: bilateral."},"equipment_type":{"type":"string","enum":["cable_machine","dumbbell","kettlebell","barbell","bodyweight","machine_other","none"]},"cable_pulley_position":{"type":"string","description":"Pulley position (only for cable_machine equipment_type)."},"apparatus_type_ids":{"type":"array","items":{"type":"string"},"description":"Optional. Apparatus types required to perform this exercise — UUIDs OR slugs (e.g. ['flat_bench','adjustable_bench']). When multiple are provided, the meaning is OR (any one satisfies the requirement). Use fitness_apparatus_types_list to discover slugs."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"primary_muscle":{"type":"string"},"exercise_type":{"type":"string"},"laterality":{"type":"string"},"equipment_type":{"type":["string","null"]},"required_apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/exercises/:id","method":"PATCH","description":"Update a user-created exercise. Cannot modify system exercises. Use to correct exercise details, change muscle groups, or archive/unarchive.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":["string","null"]},"primary_muscle":{"type":"string"},"secondary_muscles":{"type":"array","items":{"type":"string"}},"exercise_type":{"type":"string"},"measurement_type":{"type":"string"},"weight_unit":{"type":"string"},"equipment":{"type":["string","null"]},"instructions":{"type":["string","null"]},"laterality":{"type":"string","enum":["bilateral","unilateral","either"]},"equipment_type":{"type":["string","null"]},"cable_pulley_position":{"type":["string","null"]},"is_archived":{"type":"boolean"},"apparatus_type_ids":{"type":"array","items":{"type":"string"},"description":"If provided, REPLACES the existing apparatus requirements for this exercise. UUIDs or slugs accepted."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"primary_muscle":{"type":"string"},"exercise_type":{"type":"string"},"is_archived":{"type":"boolean"},"required_apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/gyms/list","method":"GET","description":"List all gyms the user owns or has been shared access to, with equipment counts. Use when a user asks 'What gyms do I have set up?' or needs to choose a gym. Returns gyms sorted with the default gym first. Each gym includes is_owner to distinguish owned vs shared gyms.","outputSchema":{"type":"object","properties":{"gyms":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"location_type":{"type":"string"},"is_default":{"type":"boolean"},"equipment_count":{"type":"number"},"is_owner":{"type":"boolean","description":"True if the authenticated user owns this gym."},"created_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/gyms/create","method":"POST","description":"Create a new gym. Use when a user wants to register a place they work out — e.g. 'Add my home gym' or 'Set up World Class as a gym'. After creation, add equipment to it.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Gym name."},"location_type":{"type":"string","enum":["home","commercial","outdoor","other"],"description":"Default: home."},"is_default":{"type":"boolean","description":"Make this the default gym. Default: false."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"location_type":{"type":"string"},"is_default":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/gyms/:id","method":"GET","description":"Get full details for a gym including all its equipment. Use when a user asks about a specific gym's setup or inventory.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"location_type":{"type":"string"},"is_default":{"type":"boolean"},"equipment":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"equipment_type":{"type":"string"},"weight_config":{"type":"object"},"notes":{"type":["string","null"]}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/gyms/:id","method":"PATCH","description":"Update a gym's name, location type, or default status. Setting is_default to true automatically clears default from other gyms.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"location_type":{"type":"string","enum":["home","commercial","outdoor","other"]},"is_default":{"type":"boolean"}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"location_type":{"type":"string"},"is_default":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/gyms/:id","method":"DELETE","description":"Delete a gym and all its equipment. Also clears active_gym_id if this gym was active. Use with caution — this is irreversible.","outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/gyms/active","method":"GET","description":"Get the user's currently active gym with all its equipment. Use when suggesting weights or equipment — the active gym determines what equipment is available for the current workout session. Each equipment row includes apparatus_types and configurations for non-weighted apparatus equipment (benches, racks, etc.). Returns null gym if no active gym is set.","outputSchema":{"type":"object","properties":{"gym":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"location_type":{"type":"string"},"is_default":{"type":"boolean"}}},"equipment":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"equipment_type":{"type":"string","description":"One of cable_machine, dumbbell, kettlebell, barbell, machine_other, apparatus."},"weight_config":{"type":["object","null"],"description":"Null for apparatus rows; otherwise type-specific shape."},"configurations":{"type":["object","null"],"description":"Apparatus-specific settings; null for non-apparatus."},"apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]}}},"description":"Apparatus types this equipment provides; empty array for non-apparatus rows."},"notes":{"type":["string","null"]}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/gyms/active","method":"POST","description":"Set or clear the active gym. Use when a user says 'I'm at the gym' or 'Switch to my home gym'. Pass gym_id to set, or null to clear. Returns the updated active gym with equipment.","inputSchema":{"type":"object","required":["gym_id"],"properties":{"gym_id":{"type":["string","null"],"description":"Gym UUID to set as active, or null to clear."}}},"outputSchema":{"type":"object","properties":{"gym":{"type":["object","null"]},"equipment":{"type":"array"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/gyms/:id/members","method":"GET","description":"List all members of a gym, including the owner. Use when a user asks 'Who has access to my gym?' or to see who shares a gym. The owner is always included with role 'owner'.","outputSchema":{"type":"object","properties":{"members":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Membership ID (absent for the owner entry)."},"user_id":{"type":"string"},"display_name":{"type":["string","null"]},"role":{"type":"string","enum":["owner","member"]},"created_at":{"type":"string"}}}},"total":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/gyms/:id/members","method":"POST","description":"Share a gym with another user by adding them as a member. Only the gym owner can do this. Use when a user says 'Share my gym with [person]' or 'Give [person] access to my home gym'. If the user is already a member, the existing membership is returned.","inputSchema":{"type":"object","required":["user_id"],"properties":{"user_id":{"type":"string","description":"UUID of the user to share the gym with."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"gym_id":{"type":"string"},"user_id":{"type":"string"},"role":{"type":"string"},"created_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/gyms/:id/members/:userId","method":"DELETE","description":"Remove a member from a gym, revoking their shared access. Only the gym owner can do this. Use when a user says 'Remove [person] from my gym' or 'Stop sharing my gym with [person]'.","outputSchema":{"type":"object","description":"Empty response with 204 status on success."},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/equipment/list","method":"GET","description":"List all equipment at a specific gym. Use when a user asks 'What dumbbells do I have at home?' or to check available weights for an exercise at a specific location.","inputSchema":{"type":"object","required":["gym_id"],"properties":{"gym_id":{"type":"string","description":"UUID of the gym to list equipment for."}}},"outputSchema":{"type":"object","properties":{"equipment":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"equipment_type":{"type":"string","description":"One of cable_machine, dumbbell, kettlebell, barbell, machine_other, apparatus."},"weight_config":{"type":["object","null"],"description":"Null for apparatus rows. Otherwise type-specific (discrete_set or cable_machine shape)."},"configurations":{"type":["object","null"],"description":"Apparatus-specific settings JSONB; null for non-apparatus equipment."},"apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]}}},"description":"Apparatus types this equipment provides (populated for apparatus rows; empty array otherwise)."},"quantity":{"type":"integer","description":"Number of this equipment available (default 1)."},"notes":{"type":["string","null"]},"summary":{"type":"string","description":"Human-readable one-line summary."}}}},"total":{"type":"integer"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/apparatus-types/list","method":"GET","description":"List all apparatus types in the reference catalog (benches, racks, bars, plyo boxes, etc.). Use BEFORE creating apparatus equipment or tagging exercises with apparatus requirements — these are the canonical slug values that fitness_equipment_create#apparatus_type_ids and fitness_exercises_create#apparatus_type_ids accept. The list is bounded (currently 10 entries) and rarely changes.","outputSchema":{"type":"object","properties":{"apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"UUID — stable identifier for joins."},"slug":{"type":"string","description":"Stable machine identifier (e.g. 'hyperextension_bench'). Treat as immutable."},"name":{"type":"string","description":"Human-readable display name."},"description":{"type":["string","null"]}}}},"total":{"type":"integer"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/equipment/create","method":"POST","description":"Add equipment to a gym. Use when a user says 'I have dumbbells from 5-30kg at home', 'Add the cable machine at my gym', or 'Register my hyperextension bench'. For weighted equipment (dumbbell/kettlebell/barbell/cable_machine/machine_other) provide weight_config. For non-weighted apparatus (benches, racks, bars, plyo boxes — equipment_type='apparatus') OMIT weight_config and provide apparatus_type_ids — a list of apparatus_types UUIDs OR slugs (e.g. ['hyperextension_bench'] or mixing UUIDs and slugs is fine). One physical piece of equipment can declare multiple apparatus types, e.g. a Reebok Deck might be ['adjustable_bench','aerobic_step','flat_bench']. Use fitness_apparatus_types_list to discover the available slugs.","inputSchema":{"type":"object","required":["gym_id","name","equipment_type"],"properties":{"gym_id":{"type":"string","description":"UUID of the gym to add equipment to."},"name":{"type":"string","description":"Equipment name (e.g. 'Adjustable Dumbbells', 'Cable Tower', 'Taurus B800 Hyperextension Bench')."},"equipment_type":{"type":"string","enum":["cable_machine","dumbbell","kettlebell","barbell","machine_other","apparatus"]},"weight_config":{"type":["object","null"],"description":"REQUIRED for non-apparatus types; MUST be omitted/null when equipment_type='apparatus'. Shape depends on type: discrete_set={type, unit, available_weights[]} or cable_machine={type, unit, block_count, pulley_positions[{name, weight_per_block, base_weight?}]}. base_weight is the cable/attachment weight before any blocks (default 0)."},"configurations":{"type":["object","null"],"description":"Optional apparatus-specific settings JSONB. Only meaningful when equipment_type='apparatus'. Examples: Reebok Deck → {step_heights_cm:[20,35]}; Taurus B800 → {incline_positions:['flat','45deg'], max_user_weight_kg:150}."},"apparatus_type_ids":{"type":"array","items":{"type":"string"},"description":"REQUIRED (≥1 entry) when equipment_type='apparatus'; MUST be empty/omitted otherwise. Each entry is either an apparatus_types UUID or a slug (e.g. 'flat_bench'). Mixed UUID/slug arrays are accepted. Use fitness_apparatus_types_list to discover slugs."},"quantity":{"type":"integer","description":"Number of this equipment available (default 1, must be >= 1)."},"notes":{"type":"string","description":"Optional notes about the equipment."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"equipment_type":{"type":"string"},"weight_config":{"type":["object","null"]},"configurations":{"type":["object","null"]},"apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]}}},"description":"Apparatus types this equipment provides (populated for apparatus rows; empty array otherwise)."},"quantity":{"type":"integer"},"gym_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/equipment/:id","method":"PATCH","description":"Update equipment details, weight configuration, apparatus tagging, or apparatus-specific configurations. Use when a user adds new weights, retags an apparatus item, or changes equipment setup. When apparatus_type_ids is provided it REPLACES the existing tags atomically.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"equipment_type":{"type":"string","enum":["cable_machine","dumbbell","kettlebell","barbell","machine_other","apparatus"]},"weight_config":{"type":["object","null"],"description":"Updated weight configuration. Pass null when changing equipment_type to 'apparatus'."},"configurations":{"type":["object","null"],"description":"Apparatus-specific settings JSONB. Only meaningful for apparatus equipment."},"apparatus_type_ids":{"type":"array","items":{"type":"string"},"description":"If provided, replaces ALL existing apparatus tags for this equipment. Each entry is a UUID or a slug. Only valid when equipment_type='apparatus' (effective)."},"quantity":{"type":"integer","description":"Number of this equipment available (must be >= 1)."},"notes":{"type":["string","null"]}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"equipment_type":{"type":"string"},"weight_config":{"type":["object","null"]},"configurations":{"type":["object","null"]},"apparatus_types":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}}}},"quantity":{"type":"integer"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/workouts/schedule","method":"POST","description":"Schedule a workout program in one of two modes. (1) ONE-OFF MODE: pass `dates` — an array of explicit YYYY-MM-DD entries (with optional starts_at/ends_at time slots). Creates concrete scheduled workout rows with snapshotted exercises from the program template; subsequent template edits do NOT change already-scheduled workouts. Use when the user names specific dates (e.g. 'schedule Push Day for Monday and Wednesday'). (2) RECURRING MODE: pass `recurrence` — `{ start_date, weeks_to_generate, days_of_week }`. Inserts a single workout_schedules row; the database trigger then materializes child task rows for each matching weekday in the window. Use when the user says 'every Monday, Wednesday, Friday for the next 4 weeks'. IMPORTANT: `recurrence.days_of_week` uses Postgres EXTRACT(DOW) convention — Sunday=0, Monday=1, Tuesday=2, Wednesday=3, Thursday=4, Friday=5, Saturday=6. Provide EXACTLY ONE of `dates` or `recurrence`, not both. The created workouts/tasks appear in fitness_workouts_upcoming.","inputSchema":{"type":"object","required":["program_id"],"properties":{"program_id":{"type":"string","description":"UUID of the program template to schedule."},"dates":{"type":"array","minItems":1,"maxItems":90,"items":{"type":"object","required":["date"],"properties":{"date":{"type":"string","format":"date","description":"Date to schedule the workout (YYYY-MM-DD). Must not be in the past."},"starts_at":{"type":"string","description":"Optional start time (HH:MM). Must pair with ends_at."},"ends_at":{"type":"string","description":"Optional end time (HH:MM). Must be after starts_at."}}},"description":"ONE-OFF MODE: array of explicit dates (and optional time slots) to schedule the program on. Mutually exclusive with `recurrence`."},"recurrence":{"type":"object","required":["start_date","weeks_to_generate","days_of_week"],"properties":{"start_date":{"type":"string","format":"date","description":"First day of the recurring window (YYYY-MM-DD). Must not be in the past."},"weeks_to_generate":{"type":"integer","minimum":1,"maximum":52,"description":"How many weeks of occurrences to generate (1-52)."},"days_of_week":{"type":"array","minItems":1,"maxItems":7,"items":{"type":"integer","minimum":0,"maximum":6},"description":"Days of the week to schedule on, using Postgres EXTRACT(DOW) convention: Sunday=0, Monday=1, Tuesday=2, Wednesday=3, Thursday=4, Friday=5, Saturday=6. Example: [1,3,5] = Mon/Wed/Fri."},"workout_name":{"type":"string","description":"Optional override for the workout name. Defaults to the program template name."}},"description":"RECURRING MODE: schedule on every matching weekday for the next N weeks. Mutually exclusive with `dates`."},"notes":{"type":"string","description":"Optional notes applied to all created workouts (one-off mode only)."}}},"outputSchema":{"type":"object","properties":{"mode":{"type":"string","enum":["one_off","recurring"],"description":"Which mode the request was processed in."},"created":{"type":"array","items":{"type":"object","properties":{"workout_id":{"type":"string"},"scheduled_for":{"type":"string","format":"date"}}},"description":"ONE-OFF MODE: list of created workout rows."},"count":{"type":"number","description":"ONE-OFF MODE: number of workouts created."},"schedule_id":{"type":"string","description":"RECURRING MODE: id of the inserted workout_schedules row."},"start_date":{"type":"string","format":"date","description":"RECURRING MODE: window start date."},"end_date":{"type":"string","format":"date","description":"RECURRING MODE: window end date (start_date + weeks_to_generate*7 - 1)."},"expected_occurrences":{"type":"integer","description":"RECURRING MODE: number of occurrences the trigger is expected to generate within the window."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/workouts/start","method":"POST","description":"Start a workout. If a program_id is provided, the start flow checks sources in order before creating a fresh workout: (1a) a one-off scheduled workout for this template with scheduled_for=today (consumed_scheduled=true, consumed_overdue=false); (1b) else the EARLIEST overdue scheduled workout for this template within the last 14 days, oldest first (consumed_scheduled=true, consumed_overdue=true) — the 14-day cap prevents resurrecting ancient stale rows when the user returns to a program after a long break; (2) else an active recurring workout_schedules row for this template where today is in [start_date, end_date] and today's day-of-week is in days_of_week (consumed_recurring=true; materializes a new workouts row with schedule_id set and the matching `schedule:<id>:<today>` task is moved to in_progress); (3) fallback — create a fresh workout from the template. Day-of-week values use Postgres EXTRACT(DOW) (Sunday=0..Saturday=6). For ad-hoc workouts (no program_id), a name is required. Always returns the workout with its exercises.","inputSchema":{"type":"object","properties":{"program_id":{"type":"string","description":"UUID of the program template. If provided, checks for a matching scheduled workout to consume before creating a new one."},"name":{"type":"string","description":"Workout name. Required for ad-hoc workouts (no program_id). Optional when starting from a program (defaults to program name)."},"notes":{"type":"string","description":"Optional workout notes."}}},"outputSchema":{"type":"object","properties":{"workout_id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"started_at":{"type":"string"},"template_id":{"type":["string","null"]},"consumed_scheduled":{"type":"boolean","description":"True if an existing one-off scheduled workout was consumed instead of creating a new one."},"consumed_overdue":{"type":"boolean","description":"True if the consumed scheduled workout was overdue (scheduled_for < today). Only relevant when consumed_scheduled is true."},"consumed_recurring":{"type":"boolean","description":"True if today's occurrence of an active recurring schedule was materialized into a workout. Mutually exclusive with consumed_scheduled."},"exercises":{"type":"array","items":{"type":"object","properties":{"exercise_id":{"type":"string"},"sort_order":{"type":"number"},"target_sets":{"type":["number","null"]},"target_reps_min":{"type":["number","null"]},"target_reps_max":{"type":["number","null"]},"target_rpe":{"type":["number","null"]}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/workouts/:workoutId/schedule","method":"PATCH","description":"Reschedule a single scheduled workout — change its date or time slot. Use when a user says 'move my Monday workout to Tuesday' or 'change the time of my scheduled push day'. Only works on workouts with status='scheduled'. Returns 409 if the workout is in_progress, completed, or cancelled.","inputSchema":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"New date (YYYY-MM-DD). Must not be in the past."},"starts_at":{"type":"string","description":"New start time (HH:MM). Must pair with ends_at."},"ends_at":{"type":"string","description":"New end time (HH:MM). Must be after starts_at."},"clear_time":{"type":"boolean","description":"If true, removes the time slot (sets starts_at and ends_at to null)."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"scheduled_for":{"type":"string","format":"date"},"scheduled_starts_at":{"type":["string","null"]},"scheduled_ends_at":{"type":["string","null"]},"status":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/workouts/:workoutId/schedule","method":"DELETE","description":"Cancel a scheduled workout by permanently deleting it and its snapshotted exercises. Use when a user says 'cancel my scheduled workout' or 'remove that workout from my calendar'. Only works on workouts with status='scheduled'. Returns 409 if the workout has already been started or completed. This is irreversible.","outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"workout_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/equipment/:id","method":"DELETE","description":"Delete a piece of equipment from a gym. Use when equipment is no longer available. This is irreversible.","outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/workouts/:workoutId","method":"GET","description":"Get full details for a single workout including all exercises, their sets, and substitution metadata. Shows both active and replaced exercises — replaced ones have is_active=false and replaced_by_id set. Use to inspect the current state of an in-progress workout or review a completed workout.","inputSchema":{"type":"object","properties":{"workoutId":{"type":"string","description":"Workout UUID (path parameter)."}},"required":["workoutId"]},"outputSchema":{"type":"object","properties":{"id":{"type":"string"},"workout_id":{"type":"string","description":"Alias used by the compact iOS workout-review payload."},"name":{"type":"string"},"status":{"type":"string"},"template_name":{"type":["string","null"]},"recommendations":{"type":"array","description":"Pending advisory recommendations relevant to this workout.","items":{"type":"object","properties":{"id":{"type":"string"},"recommendation_type":{"type":"string","enum":["increase_load","increase_reps","maintain","reduce_load","reduce_volume","increase_volume","change_exercise","adjust_rpe","technique_focus","recovery","other"]},"status":{"type":"string","enum":["pending","accepted","dismissed","superseded"]},"program_id":{"type":["string","null"]},"upcoming_workout_id":{"type":["string","null"]},"source_workout_id":{"type":"string"},"source_workout_exercise_id":{"type":["string","null"]},"exercise_id":{"type":"string"},"target":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"target_sets":{"type":"integer","minimum":1,"maximum":100},"target_reps":{"type":"integer","minimum":1,"maximum":1000},"target_rep_min":{"type":"integer","minimum":1,"maximum":1000},"target_rep_max":{"type":"integer","minimum":1,"maximum":1000},"target_weight_kg":{"type":"number","minimum":0,"maximum":2000},"target_rpe":{"type":"number","minimum":1,"maximum":10},"target_rpe_min":{"type":"number","minimum":1,"maximum":10},"target_rpe_max":{"type":"number","minimum":1,"maximum":10},"target_rest_seconds":{"type":"integer","minimum":0,"maximum":86400},"target_duration_seconds":{"type":"integer","minimum":1,"maximum":86400},"instruction":{"type":"string","maxLength":500}}},"reasoning":{"type":"string"},"created_by":{"type":"string","enum":["external_coach","user","system"]},"last_action_by":{"type":["string","null"],"enum":["external_coach","user","system",null]},"supersedes_id":{"type":["string","null"]},"superseded_by_id":{"type":["string","null"]},"accepted_at":{"type":["string","null"]},"dismissed_at":{"type":["string","null"]},"superseded_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}},"exercises":{"type":"array","items":{"type":"object"}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/fitness/workouts/:workoutId/exercises/:workoutExerciseId/swap","method":"POST","description":"Swap an exercise mid-workout. Replaces one exercise with another while preserving already-completed sets on the original. The template/program is NOT modified — the swap only affects this workout instance. Use when a user reports pain, equipment is unavailable, or they want an alternative exercise targeting the same muscle group.","inputSchema":{"type":"object","properties":{"workoutId":{"type":"string","description":"Workout UUID (path parameter). Must be in_progress."},"workoutExerciseId":{"type":"string","description":"The workout_exercise UUID to replace (path parameter)."},"new_exercise_id":{"type":"string","description":"The exercise catalog UUID to swap in."},"reason":{"type":"string","description":"Optional reason for the swap (e.g. 'Pain/discomfort', 'Equipment unavailable')."}},"required":["workoutId","workoutExerciseId","new_exercise_id"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"fitness"},{"path":"/api/v1/fitness/workouts/:workoutId/exercises/:workoutExerciseId/suggest-swap","method":"GET","description":"Suggest substitute exercises for a mid-workout swap. Returns up to 10 exercises targeting the same primary muscle group as the exercise being replaced. User-created exercises are listed first, then system exercises. Call this BEFORE fitness_workouts_exercises_swap to help the user choose a replacement.","inputSchema":{"type":"object","properties":{"workoutId":{"type":"string","description":"Workout UUID (path parameter)."},"workoutExerciseId":{"type":"string","description":"The workout_exercise UUID to find replacements for (path parameter)."},"equipment_type":{"type":"string","enum":["cable_machine","dumbbell","kettlebell","barbell","bodyweight","machine_other","none"],"description":"Optional filter to narrow suggestions to a specific equipment type."},"exclude_exercise_ids":{"type":"string","description":"Optional comma-separated list of exercise UUIDs to exclude from suggestions (e.g. exercises already in this workout)."}},"required":["workoutId","workoutExerciseId"]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"fitness"},{"path":"/api/v1/notes/search","method":"GET","description":"Full-text search across all notes using PostgreSQL tsvector. Use when a user wants to find notes by keyword — for example 'find my notes about Iceland' or 'search for meeting notes'. Returns ranked results with content snippets highlighting the matched terms. Title matches rank higher than content matches. Supports filtering by notebook and archive status.","inputSchema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search terms. Words are ANDed together with prefix matching (e.g. 'meet island' matches 'meeting in Iceland')."},"notebook_id":{"type":"string","description":"Optional UUID to limit search to a specific notebook."},"tag":{"type":"string","description":"Filter by tag name (case-insensitive exact match)."},"is_archived":{"type":"boolean","description":"Whether to search archived notes. Defaults to false (active notes only)."},"limit":{"type":"number","description":"Maximum results to return (default: 20, max: 50)."},"offset":{"type":"number","description":"Pagination offset (default: 0)."}}},"outputSchema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"snippet":{"type":"string","description":"Content excerpt with matched terms highlighted."},"notebook":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"tags":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"color":{"type":["string","null"]}}}},"is_pinned":{"type":"boolean"},"is_archived":{"type":"boolean"},"updated_at":{"type":"string"}}}},"count":{"type":"number"},"has_more":{"type":"boolean"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"notes"},{"path":"/api/v1/notes/:note_id","method":"GET","description":"Get a single note with its full Markdown content, notebook info, tags, and attachment count. Use when a user asks about a specific note or wants to read its content — for example 'show me that note about the API design' or 'what does my grocery list say?'.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"content":{"type":"string","description":"Full Markdown content of the note."},"notebook":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":["string","null"]}}}},"attachment_count":{"type":"number"},"is_pinned":{"type":"boolean"},"is_archived":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"url":{"type":"string","description":"Direct link to the note in the Notes app."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"notes"},{"path":"/api/v1/notes","method":"GET","description":"List notes with filtering, sorting, and pagination. Returns summaries with a ~200-character content preview rather than full content. Use when a user asks to see their notes — for example 'show my pinned notes', 'what's in my Work notebook?', or 'list my recent notes'. Supports filtering by notebook, tag, pinned status, and archive status.","inputSchema":{"type":"object","properties":{"notebook_id":{"type":"string","description":"Filter to notes in a specific notebook (UUID)."},"tag":{"type":"string","description":"Filter by tag name (exact match, case-insensitive)."},"is_pinned":{"type":"boolean","description":"If true, only pinned notes. If false, only unpinned."},"is_archived":{"type":"boolean","description":"Whether to show archived notes. Defaults to false (active notes only)."},"sort":{"type":"string","enum":["updated_at","created_at","title"],"description":"Sort field. Defaults to updated_at."},"order":{"type":"string","enum":["asc","desc"],"description":"Sort direction. Defaults to desc."},"limit":{"type":"number","description":"Maximum notes to return (default: 50, max: 100)."},"offset":{"type":"number","description":"Pagination offset (default: 0)."}}},"outputSchema":{"type":"object","properties":{"notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"preview":{"type":"string","description":"First ~200 characters of content."},"notebook":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"tags":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"color":{"type":["string","null"]}}}},"attachment_count":{"type":"number"},"is_pinned":{"type":"boolean"},"is_archived":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}},"count":{"type":"number"},"has_more":{"type":"boolean"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"notes"},{"path":"/api/v1/notes/notebooks","method":"GET","description":"Get all notebooks as a nested tree structure with note counts. Use when a user asks about their notebook organization — for example 'what notebooks do I have?' or 'show me my note categories'. Returns a hierarchical tree where each notebook includes its children and the number of notes it contains.","outputSchema":{"type":"object","properties":{"notebooks":{"type":"array","description":"Top-level notebooks with nested children.","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"parent_id":{"type":["string","null"]},"sort_order":{"type":"number"},"note_count":{"type":"number"},"children":{"type":"array","description":"Child notebooks (recursive structure).","items":{"type":"object"}}}}},"total_notebooks":{"type":"number"},"total_notes":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"notes"},{"path":"/api/v1/notes/create","method":"POST","description":"Create a new note with optional content, notebook placement, and tags. Use when a user wants to save information — for example 'save a note about the meeting' or 'create a note in my Work notebook'. Tags are matched by name (case-insensitive) and created automatically if they don't exist.","inputSchema":{"type":"object","properties":{"title":{"type":"string","description":"Note title. Defaults to 'Untitled' if not provided."},"content":{"type":"string","description":"Markdown content of the note."},"notebook_id":{"type":"string","description":"UUID of the notebook to place the note in."},"tags":{"type":"array","items":{"type":"string"},"description":"Tag names to assign. Tags are created automatically if they don't exist."}}},"outputSchema":{"type":"object","properties":{"note_id":{"type":"string"},"title":{"type":"string"},"notebook_id":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"},"description":"Names of assigned tags."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notes"},{"path":"/api/v1/notes/:note_id","method":"PATCH","description":"Update an existing note's title, content, notebook, pinned/archived status, or tags. Only provided fields are changed — omitted fields stay as-is. Use when a user wants to edit a note — for example 'rename that note', 'move it to my Work notebook', 'pin that note', or 'update the content'. When tags are provided, they replace all existing tags on the note.","inputSchema":{"type":"object","properties":{"title":{"type":"string","description":"New title for the note."},"content":{"type":"string","description":"New Markdown content (replaces existing content entirely)."},"notebook_id":{"type":["string","null"],"description":"Move to a different notebook (UUID), or null to remove from notebook."},"is_pinned":{"type":"boolean","description":"Pin or unpin the note."},"is_archived":{"type":"boolean","description":"Archive or unarchive the note."},"tags":{"type":"array","items":{"type":"string"},"description":"Replacement tag names. Replaces all existing tags. Tags are created automatically if they don't exist."}}},"outputSchema":{"type":"object","properties":{"note_id":{"type":"string"},"title":{"type":"string"},"updated_fields":{"type":"array","items":{"type":"string"},"description":"List of field names that were changed."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notes"},{"path":"/api/v1/notes/append/:note_id","method":"POST","description":"Append content to an existing note, separated by a double newline. Use when a user wants to add to a note without replacing existing content — for example 'add this to my meeting notes' or 'append a new section to that document'. This is safer than PATCH for incremental additions because it doesn't require knowing the current content.","inputSchema":{"type":"object","required":["content"],"properties":{"content":{"type":"string","description":"Markdown content to append. Will be separated from existing content by two newlines."}}},"outputSchema":{"type":"object","properties":{"note_id":{"type":"string"},"title":{"type":"string"},"content_length":{"type":"number","description":"Total length of the note content after appending."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notes"},{"path":"/api/v1/notes/:note_id","method":"DELETE","description":"Permanently delete a note and all its attachments. This is a hard delete — the note cannot be recovered. Consider using PATCH to set is_archived=true for soft deletion instead. Use only when a user explicitly asks to permanently delete a note.","outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"note_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notes"},{"path":"/api/v1/notes/notebooks/create","method":"POST","description":"Create a new notebook for organizing notes. Notebooks can be nested inside other notebooks using parent_id. Use when a user wants to organize their notes — for example 'create a Work notebook' or 'add a sub-notebook for Q1 inside Projects'.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Notebook name."},"parent_id":{"type":"string","description":"UUID of the parent notebook for nesting. Omit for a top-level notebook."}}},"outputSchema":{"type":"object","properties":{"notebook_id":{"type":"string"},"name":{"type":"string"},"parent_id":{"type":["string","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notes"},{"path":"/api/v1/budgeting/categories","method":"GET","description":"List variable cost categories. Each category has a label, priority tier (essential/discretionary), sort order, and status (active/archived). Filter by status with ?status=active.","inputSchema":{"type":"object","required":["family_id"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit owning household; caller must be an active parent."},"status":{"type":"string","enum":["active","archived"],"description":"Filter by status. Omit for all."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/categories","method":"POST","description":"Create a new variable cost category.","inputSchema":{"type":"object","required":["family_id","label"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit owning household; caller must be an active parent."},"label":{"type":"string","description":"Category label (e.g. 'Matvörur')."},"priority_tier":{"type":"string","enum":["essential","discretionary"],"description":"Priority tier. Defaults to 'discretionary'."},"default_monthly_target":{"type":"number","description":"Default monthly budget target in ISK. Defaults to 0."},"sort_order":{"type":"integer","description":"Display sort order. Defaults to the end of this household and priority tier."},"status":{"type":"string","enum":["active","archived"],"description":"Status. Defaults to 'active'."},"account_id":{"type":"string","description":"Optional accounting key ID."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/categories/:category_id","method":"PATCH","description":"Update a variable cost category.","inputSchema":{"type":"object","required":["family_id"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit owning household; caller must be an active parent."},"label":{"type":"string","description":"Category label."},"priority_tier":{"type":"string","enum":["essential","discretionary"]},"default_monthly_target":{"type":"number"},"sort_order":{"type":"integer"},"status":{"type":"string","enum":["active","archived"]},"account_id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/categories/:category_id","method":"DELETE","description":"Archive a household category, preserving its ID and historical references. Reactivate with PATCH status=active.","inputSchema":{"type":"object","required":["family_id"],"properties":{"family_id":{"type":"string","format":"uuid"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/bank-accounts","method":"GET","description":"List bank accounts. Returns active accounts by default. Use include_closed=true for all.","inputSchema":{"type":"object","properties":{"include_closed":{"type":"boolean"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/bank-accounts","method":"POST","description":"Create a new bank account (checking, savings, cash, or other).","inputSchema":{"type":"object","required":["label"],"properties":{"label":{"type":"string","description":"Account name (e.g. 'Checking - Landsbankinn')."},"account_type":{"type":"string","enum":["checking","savings","cash","other"]},"currency":{"type":"string","description":"Currency code. Defaults to ISK."},"current_balance":{"type":"number","description":"Current balance."},"is_primary":{"type":"boolean","description":"Set as primary account."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/bank-accounts/:bank_account_id","method":"GET","description":"Get a bank account with recent balance history.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/bank-accounts/:bank_account_id","method":"PATCH","description":"Update a bank account.","inputSchema":{"type":"object","properties":{"label":{"type":"string"},"account_type":{"type":"string"},"current_balance":{"type":"number"},"is_primary":{"type":"boolean"},"status":{"type":"string","enum":["active","closed"]},"confirmation_frequency_days":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/bank-accounts/:bank_account_id","method":"DELETE","description":"Delete a bank account.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/cards","method":"GET","description":"List debit cards and credit accounts, including individual linked cards. Balances and limits belong to credit accounts. Returns active records by default.","inputSchema":{"type":"object","properties":{"include_closed":{"type":"boolean"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/cards","method":"POST","description":"Create a debit card or credit account. Create one credit account per shared balance, not per physical card. Debit cards require linked_bank_account_id. Credit accounts require billing_day (cycle closing day) and payment_due_day.","inputSchema":{"type":"object","required":["label","card_type"],"properties":{"family_id":{"type":"string","description":"Active parent household; required if you belong to multiple households."},"label":{"type":"string"},"card_type":{"type":"string","enum":["debit","credit"]},"linked_bank_account_id":{"type":"string","description":"Required for debit cards."},"credit_limit":{"type":"number"},"current_balance":{"type":"number"},"billing_day":{"type":"number","description":"Required for credit cards (1-31)."},"payment_due_day":{"type":"number","description":"Required for credit cards (1-31)."},"payment_due_adjustment":{"type":"string","enum":["calendar_day","next_weekday"],"description":"Move weekend due dates to Monday with next_weekday; holidays are not adjusted."},"auto_pay":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/cards/:card_id","method":"GET","description":"Get a single card by ID.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/cards/:card_id","method":"PATCH","description":"Update a card.","inputSchema":{"type":"object","properties":{"label":{"type":"string"},"status":{"type":"string","enum":["active","closed"]},"current_balance":{"type":"number"},"credit_limit":{"type":"number"},"billing_day":{"type":"number"},"payment_due_day":{"type":"number"},"payment_due_adjustment":{"type":"string","enum":["calendar_day","next_weekday"]},"auto_pay":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/cards/:card_id","method":"DELETE","description":"Delete a card.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/debts","method":"GET","description":"List all debts (loans, mortgages, etc.).","outputSchema":{"type":"object","properties":{"debts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"lender":{"type":"string"},"loan_type":{"type":"string"},"payment_schedule":{"type":"string"},"initial_amount":{"type":"number"},"current_balance":{"type":"number"},"term_months":{"type":"number"},"payment_number":{"type":"integer","description":"Current payment number (payment X of Y)."},"total_payments":{"type":"integer","description":"Total number of payments (payment X of Y)."},"start_date":{"type":"string"},"created_at":{"type":"string"}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/debts","method":"POST","description":"Register a new debt from your latest loan invoice. Provide the remaining balance, interest rate, and payment number. Supports both full registration and invoice-based registration.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Debt name (e.g. 'Mortgage - Arion')."},"lender":{"type":"string"},"loan_type":{"type":"string","enum":["indexed","non_indexed"],"description":"Defaults to non_indexed."},"payment_schedule":{"type":"string","enum":["annuity","linear"],"description":"Defaults to annuity."},"initial_amount":{"type":"number","description":"Original loan amount in ISK."},"current_balance":{"type":"number","description":"Current outstanding balance."},"term_months":{"type":"number","description":"Loan term in months."},"start_date":{"type":"string","description":"Loan start date (YYYY-MM-DD)."},"asset_id":{"type":"string","description":"Optional linked asset ID."},"index_code":{"type":"string","description":"CPI index code for indexed loans."},"payment_number":{"type":"integer","description":"Current payment number from the latest invoice (payment X of Y)."},"total_payments":{"type":"integer","description":"Total number of payments from the latest invoice (payment X of Y)."},"interest_rate":{"type":"number","description":"Current annual interest rate as shown on the invoice. Creates an initial interest period automatically."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/debts/:debt_id","method":"GET","description":"Get a debt with its interest periods and upcoming payment schedule.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"lender":{"type":"string"},"loan_type":{"type":"string"},"payment_schedule":{"type":"string"},"initial_amount":{"type":"number"},"current_balance":{"type":"number"},"term_months":{"type":"number"},"payment_number":{"type":"integer","description":"Current payment number (payment X of Y)."},"total_payments":{"type":"integer","description":"Total number of payments (payment X of Y)."},"start_date":{"type":"string"},"interest_periods":{"type":"array","items":{"type":"object"}},"upcoming_payments":{"type":"array","items":{"type":"object"}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/debts/:debt_id","method":"PATCH","description":"Update a debt record.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"lender":{"type":"string"},"current_balance":{"type":"number"},"term_months":{"type":"number"},"loan_type":{"type":"string"},"payment_schedule":{"type":"string"},"index_code":{"type":"string"},"payment_number":{"type":"integer","description":"Current payment number (payment X of Y)."},"total_payments":{"type":"integer","description":"Total number of payments (payment X of Y)."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/debts/:debt_id","method":"DELETE","description":"Delete a debt and its associated interest periods and payment schedule.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/assets","method":"GET","description":"List assets with current valuations. Filter by status (default: owned).","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["owned","disposed"]}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/assets","method":"POST","description":"Create a new asset (property, vehicle, investment, etc.).","inputSchema":{"type":"object","required":["name","asset_type","purchase_price","purchase_date"],"properties":{"name":{"type":"string"},"asset_type":{"type":"string","description":"e.g. 'real_estate', 'vehicle', 'investment'"},"purchase_price":{"type":"number"},"purchase_date":{"type":"string","description":"YYYY-MM-DD"},"currency":{"type":"string"},"financing_notes":{"type":"string"},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/assets/:asset_id","method":"GET","description":"Get an asset with valuation history and linked debts.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/assets/:asset_id","method":"PATCH","description":"Update an asset.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"asset_type":{"type":"string"},"status":{"type":"string","enum":["owned","disposed"]},"disposal_date":{"type":"string"},"disposal_amount":{"type":"number"},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/assets/:asset_id","method":"DELETE","description":"Delete an asset.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/summary","method":"GET","description":"Get a computed financial overview: bank balances, total debt, total assets, net worth, monthly income/costs, and net cash flow. All values in ISK.","outputSchema":{"type":"object","properties":{"bank_balance":{"type":"number"},"credit_owed":{"type":"number"},"total_liquid":{"type":"number"},"total_assets":{"type":"number"},"total_debt":{"type":"number"},"net_worth":{"type":"number"},"monthly_income":{"type":"number"},"monthly_costs":{"type":"number"},"net_monthly_cash_flow":{"type":"number"},"account_count":{"type":"number"},"card_count":{"type":"number"},"debt_count":{"type":"number"},"asset_count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/settings","method":"GET","description":"Get budgeting settings (currency, payment defaults, thresholds).","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/settings","method":"PATCH","description":"Update budgeting settings. Creates settings row if none exists.","inputSchema":{"type":"object","properties":{"currency":{"type":"string"},"default_payment_method_type":{"type":"string"},"default_payment_method_id":{"type":"string"},"minimum_balance_threshold":{"type":"number"},"large_outflow_lookahead_days":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/evidence","method":"GET","description":"List evidence attachments (receipts, statements, invoices) for a given transaction. Requires entity_type and entity_id. Returns file metadata with signed download URLs.","inputSchema":{"type":"object","required":["entity_type","entity_id"],"properties":{"entity_type":{"type":"string","enum":["revenue_transaction","fixed_cost_transaction","variable_cost_transaction","asset_transaction","credit_card_payment","debt_payment"],"description":"Transaction type to look up evidence for."},"entity_id":{"type":"string","description":"UUID of the transaction."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/evidence","method":"POST","description":"Attach evidence (receipt, statement, invoice) to a transaction. Provide entity_type, entity_id, file details, and an optional label.","inputSchema":{"type":"object","required":["entity_type","entity_id","file_name","file_path"],"properties":{"entity_type":{"type":"string","enum":["revenue_transaction","fixed_cost_transaction","variable_cost_transaction","asset_transaction","credit_card_payment","debt_payment"],"description":"Transaction type."},"entity_id":{"type":"string","description":"UUID of the transaction."},"file_name":{"type":"string","description":"Original file name."},"file_path":{"type":"string","description":"Storage path of the uploaded file."},"file_size":{"type":"number","description":"File size in bytes."},"mime_type":{"type":"string","description":"MIME type (e.g. image/jpeg, application/pdf)."},"label":{"type":"string","description":"Optional label (e.g. 'receipt', 'bank statement', 'invoice')."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/evidence/:evidence_id","method":"GET","description":"Get a single evidence attachment with a signed download URL.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/evidence/:evidence_id","method":"DELETE","description":"Delete an evidence attachment. Removes both the database record and the file from storage.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/monthly-budgets","method":"GET","description":"List monthly budgets with approved total spent per budget. Filter by year, status, or specific month (YYYY-MM).","inputSchema":{"type":"object","required":["family_id"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit household; active parent access required."},"year":{"type":"string","description":"Filter by year (YYYY)."},"month":{"type":"string","description":"Get a specific month (YYYY-MM)."},"status":{"type":"string","description":"Filter by status (draft, active, closed)."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/monthly-budgets","method":"POST","description":"Create or get a household monthly budget (idempotent). Does not change existing amount, status or spending limits. Use PATCH with expected_updated_at for updates.","inputSchema":{"type":"object","required":["family_id","budget_month"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit household; active parent access required."},"budget_month":{"type":"string","description":"Month (YYYY-MM or YYYY-MM-DD)."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/monthly-budgets/:budget_id","method":"GET","description":"Get a household monthly budget with complete allocations, approved spending and split-category totals. Unmatched amounts appear as unallocated.","inputSchema":{"type":"object","required":["family_id"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Selected household; active parent access required."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/monthly-budgets/:budget_id","method":"PATCH","description":"Update a monthly budget in the selected household (available_amount, status).","inputSchema":{"type":"object","required":["family_id","expected_updated_at"],"properties":{"expected_updated_at":{"type":"string","description":"Exact updated_at value from the latest budget read; stale writes return 409."},"family_id":{"type":"string","format":"uuid","description":"Selected household; active parent access required."},"available_amount":{"type":"number"},"status":{"type":"string","description":"Budget status (draft, active, closed)."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/merchants","method":"GET","description":"List merchants. Filter by name search, merchant_type, or include inactive.","inputSchema":{"type":"object","properties":{"search":{"type":"string","description":"Search merchant name (case-insensitive)."},"merchant_type":{"type":"string","description":"Filter by type (e.g. restaurant, grocery, liquor_store)."},"include_inactive":{"type":"boolean","description":"Include inactive merchants (default false)."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/merchants","method":"POST","description":"Create a new merchant.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Merchant name."},"location":{"type":"string","description":"Location description."},"merchant_type":{"type":"string","description":"Type (e.g. restaurant, grocery, gas_station)."},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/merchants/:merchant_id","method":"GET","description":"Get a merchant with recent approved transactions.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/merchants/:merchant_id","method":"PATCH","description":"Update a merchant.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"location":{"type":"string"},"merchant_type":{"type":"string"},"notes":{"type":"string"},"is_active":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs","method":"GET","description":"List variable cost transactions. Filter by month (YYYY-MM), category_id, merchant_id, card_id, status. Defaults to all transactions, sorted by date descending.","inputSchema":{"type":"object","required":["family_id"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit household; active parent access required."},"month":{"type":"string","description":"Filter by month (YYYY-MM). Returns all if omitted."},"category_id":{"type":"string","description":"Filter by default category ID."},"merchant_id":{"type":"string","description":"Filter by merchant ID."},"card_id":{"type":"string","description":"Filter by card ID."},"status":{"type":"string","description":"Filter by status: suggestion, approved, rejected."},"limit":{"type":"number","description":"Max results (default 100)."},"offset":{"type":"number","description":"Pagination offset (default 0)."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs","method":"POST","description":"Create a variable cost transaction. Auto-resolves monthly budget. Optionally include line_items array for multi-category receipts. Set status='suggestion' for AI-created transactions pending review.","inputSchema":{"type":"object","required":["family_id","transaction_date","total_amount","category_id"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit household; active parent access required."},"transaction_date":{"type":"string","description":"Transaction date (YYYY-MM-DD)."},"total_amount":{"type":"number","description":"Total amount."},"category_id":{"type":"string","description":"Default category ID."},"vendor_description":{"type":"string","description":"Free-text vendor/merchant description."},"card_id":{"type":"string","description":"Payment card ID."},"merchant_id":{"type":"string","description":"Merchant ID."},"account_id":{"type":"string","description":"Accounting key ID for GL posting."},"status":{"type":"string","description":"Transaction status: suggestion (AI-created), approved (default), rejected."},"currency":{"type":"string","description":"Currency code (default ISK)."},"notes":{"type":"string","description":"Free-text notes."},"created_by":{"type":"string","description":"UUID of the creating user/agent. Null means owning user."},"payment_method_type":{"type":"string","description":"Payment method type: card or bank_account."},"payment_method_id":{"type":"string","description":"UUID of the card or bank account used for payment."},"line_items":{"type":"array","description":"Optional line items for multi-account transactions.","items":{"type":"object","required":["description","amount"],"properties":{"description":{"type":"string","description":"Line item description."},"amount":{"type":"number","description":"Line total amount."},"account_id":{"type":"string","description":"GL account ID from accounting_keys."},"quantity":{"type":"number","description":"Quantity (default 1)."},"unit_price":{"type":"number","description":"Price per unit."},"vat_rate":{"type":"number","description":"VAT rate as decimal (e.g., 0.11)."},"vat_amount":{"type":"number","description":"VAT amount included in line total."},"sort_order":{"type":"number","description":"Display order (default: array index)."},"notes":{"type":"string","description":"Line item notes."}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id","method":"GET","description":"Get a variable cost transaction with line items, evidence, merchant, card, and budget details.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id","method":"PATCH","description":"Update a variable cost transaction. When line_items array is provided, all existing line items are replaced (delete old, insert new).","inputSchema":{"type":"object","properties":{"transaction_date":{"type":"string"},"total_amount":{"type":"number"},"vendor_description":{"type":"string"},"category_id":{"type":"string","description":"Default category ID."},"card_id":{"type":"string"},"merchant_id":{"type":"string"},"account_id":{"type":"string"},"currency":{"type":"string","description":"Currency code."},"notes":{"type":"string","description":"Free-text notes."},"payment_method_type":{"type":"string","description":"Payment method type: card or bank_account."},"payment_method_id":{"type":"string","description":"UUID of the card or bank account."},"line_items":{"type":"array","description":"Replace all line items. Omit to keep existing line items unchanged.","items":{"type":"object","required":["description","amount"],"properties":{"description":{"type":"string"},"amount":{"type":"number"},"account_id":{"type":"string","description":"GL account ID from accounting_keys."},"quantity":{"type":"number"},"unit_price":{"type":"number"},"vat_rate":{"type":"number"},"vat_amount":{"type":"number"},"sort_order":{"type":"number"},"notes":{"type":"string"}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id","method":"DELETE","description":"Delete a variable cost transaction. Cascades to line items and removes associated evidence files.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id/status","method":"PATCH","description":"Change the status of a variable cost transaction (approve or reject). Sets status_changed_at automatically. Use this for the AI suggestion review workflow.","inputSchema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","description":"New status: approved or rejected."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id/line-items","method":"GET","description":"List line items for a variable cost transaction with category labels.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id/line-items","method":"POST","description":"Add a line item to a variable cost transaction. Automatically sets has_line_items=true on the parent transaction.","inputSchema":{"type":"object","required":["description","amount"],"properties":{"description":{"type":"string","description":"Line item description."},"amount":{"type":"number","description":"Line total amount."},"account_id":{"type":"string","description":"GL account ID from accounting_keys."},"quantity":{"type":"number","description":"Quantity (default 1)."},"unit_price":{"type":"number","description":"Price per unit."},"vat_rate":{"type":"number","description":"VAT rate as decimal (e.g., 0.11)."},"vat_amount":{"type":"number","description":"VAT amount included in line total."},"sort_order":{"type":"number","description":"Display order (default 0)."},"notes":{"type":"string","description":"Line item notes."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id/line-items/:line_item_id","method":"DELETE","description":"Delete a line item from a variable cost transaction.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id/line-items/bulk","method":"PATCH","description":"Bulk update line item GL accounts for a transaction. Use during suggestion review to reassign accounts before approving. Validates that all line items belong to the transaction and all account IDs exist.","inputSchema":{"type":"object","required":["updates"],"properties":{"updates":{"type":"array","description":"Array of line item account updates.","items":{"type":"object","required":["line_item_id","account_id"],"properties":{"line_item_id":{"type":"string","description":"Line item UUID."},"account_id":{"type":"string","description":"GL account UUID from accounting_keys."}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/suggestions","method":"GET","description":"List variable cost transactions with status='suggestion' for review. Returns enriched data including merchant, card, category, monthly budget, line items with category labels, and evidence with signed URLs. Paginated with limit/offset.","inputSchema":{"type":"object","required":["family_id"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit household; active parent access required."},"limit":{"type":"integer","description":"Max results to return. Default 50."},"offset":{"type":"integer","description":"Offset for pagination. Default 0."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id/line-items/:line_item_id/ingredient","method":"PATCH","description":"Link or unlink a line item to/from an ingredient in public.ingredients. Setting ingredient_id to null clears the link. Used during transaction review to map grocery line items to ingredients for price tracking.","inputSchema":{"type":"object","properties":{"ingredient_id":{"type":["string","null"],"description":"Ingredient UUID, or null to clear the link."},"ingredient_quantity":{"type":"number","description":"Quantity of the ingredient in this line item."},"ingredient_unit":{"type":"string","description":"Unit of measure (l, kg, stk, etc.)."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/variable-costs/:transaction_id/approve-with-ingredients","method":"POST","description":"Atomically approve a suggestion transaction while creating ingredient mappings and price observations. For each mapping: optionally creates a new ingredient, links the line item, and records a price observation (which auto-updates the ingredient's reference_price via trigger). Use when approving a grocery receipt and mapping its line items to ingredients.","inputSchema":{"type":"object","required":["ingredient_mappings"],"properties":{"ingredient_mappings":{"type":"array","items":{"type":"object","required":["line_item_id","quantity","unit"],"properties":{"line_item_id":{"type":"string","description":"Line item UUID."},"ingredient_id":{"type":"string","description":"Existing ingredient UUID (omit if using create_new)."},"quantity":{"type":"number","description":"Quantity purchased (e.g. 1 for 1L)."},"unit":{"type":"string","description":"Unit of measure (l, kg, stk, etc.)."},"create_new":{"type":"object","properties":{"name":{"type":"string","description":"New ingredient name."},"category":{"type":"string","description":"Category."},"default_unit":{"type":"string","description":"Default unit."}},"required":["name"],"description":"Create a new ingredient instead of referencing an existing one."}}},"description":"Array of line item → ingredient mappings."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/receipts/pending","method":"GET","description":"List receipts waiting in the upload inbox (pending_uploads where app='budgeting' and status='pending'). Use this to discover what receipts need to be extracted into suggestion transactions. Returns nested file metadata for each upload.","outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/receipts/:upload_id/extraction-context","method":"GET","inputSchema":{"type":"object","required":["family_id"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Selected household UUID. Requires active parent membership."}}},"description":"Get everything needed to extract line items from a pending receipt: signed URLs to view the receipt file(s), the selected household’s active detail expense accounts, the user's default currency, the prompt_version string, and the instructions text. After extracting, post the parsed line items to /receipts/:upload_id/promote. This endpoint is read-only and does NOT mark the upload as processed.","outputSchema":{"type":"object","properties":{"upload":{"type":"object"},"files":{"type":"array","items":{"type":"object","properties":{"signed_url":{"type":"string","description":"Time-limited URL valid for 1 hour. Use this to view the receipt image."}}}},"accounts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"}}}},"default_currency":{"type":"string"},"prompt_version":{"type":"string"},"instructions":{"type":"string","description":"Instructions text describing what to extract and the JSON shape to post back."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/receipts/:upload_id/promote","method":"POST","description":"Post the parsed line items extracted from a receipt back to the gateway. Atomically creates a variable_cost_transactions row in status='suggestion', the variable_cost_line_items rows with source='ai_initial', the line_item_ai_predictions snapshot rows linked back via ai_prediction_id, the transaction_evidence row pointing the receipt at the new transaction, and marks the pending_uploads row as completed. The user reviews and approves the suggestion in the budgeting app afterwards. model_version is REQUIRED so eval data is honest about which model produced each prediction.","inputSchema":{"type":"object","required":["family_id","model_version","line_items"],"properties":{"family_id":{"type":"string","format":"uuid","description":"Explicit household; active parent access required."},"model_version":{"type":"string","description":"Exact identifier of the model that produced these line items (e.g. 'claude-sonnet-4-6'). Recorded on every prediction row."},"prompt_version":{"type":"string","description":"Optional prompt version override. Defaults to the PROMPT_VERSION returned by /extraction-context."},"vendor_name":{"type":"string","description":"Vendor/merchant name from the receipt."},"transaction_date":{"type":"string","description":"Transaction date (YYYY-MM-DD). Defaults to today if omitted."},"currency":{"type":"string","description":"ISO currency code (e.g. ISK, USD, EUR). Defaults to ISK."},"line_items":{"type":"array","description":"Array of extracted line items. Each item must include description, quantity, unit_price, discount, amount, account_id, and per-field confidence (high|medium|low).","items":{"type":"object","required":["description","quantity","unit_price","discount","amount","account_id","confidence"],"properties":{"description":{"type":"string"},"quantity":{"type":"number"},"unit_price":{"type":"number"},"discount":{"type":"number"},"amount":{"type":"number"},"account_id":{"type":"string","description":"Expense account UUID from /extraction-context.accounts."},"confidence":{"type":"object","required":["description","quantity","unit_price","discount","amount","account_id"],"properties":{"description":{"type":"string","enum":["high","medium","low"]},"quantity":{"type":"string","enum":["high","medium","low"]},"unit_price":{"type":"string","enum":["high","medium","low"]},"discount":{"type":"string","enum":["high","medium","low"]},"amount":{"type":"string","enum":["high","medium","low"]},"account_id":{"type":"string","enum":["high","medium","low"]}}}}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/statements/promote","method":"POST","description":"Ingest an Íslandsbanki card-statement (kortayfirlit) CSV into the data spine (Phase 3, the second source type after receipts). Deterministically parses the export — no model — and, per settled charge, creates one spine.facts (fact_type='monetary', status='pending', no line items) plus a spine.monetary_facts row, under one spine.sources row (source_type='statement_upload'). Pending '0 kr.' authorizations are skipped. Idempotent: re-uploading the same statement (or an overlapping export) does not duplicate facts. All spine writes are gated by SPINE_DUAL_WRITE_ENABLED; with the flag off it parses and reports counts but writes nothing. Merchant/entity resolution and statement↔receipt matching happen in a later step.","inputSchema":{"type":"object","required":["csv"],"properties":{"csv":{"type":"string","description":"Raw Íslandsbanki card-statement CSV contents (header: Dagsetning,Mótaðili,Tegund,Upphæð í erlendum gjaldmiðli,Upphæð)."},"filename":{"type":"string","description":"Optional original filename, recorded on the spine.sources origin."},"card_id":{"type":"string","description":"Optional budgeting.cards UUID this statement belongs to, recorded in fact provenance for later matching."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/statements/matches/propose","method":"POST","description":"Generate statement↔receipt match candidates. For each unmatched statement card-charge fact, finds receipt facts with the same ISK amount within a date window, scores them (entity/card/day agreement), and writes qualifying pairs as PROPOSED spine.fact_links (relation='matches'). Nothing is signed or applied to any total — a human signs afterwards. Idempotent; flag-gated by SPINE_DUAL_WRITE_ENABLED.","inputSchema":{"type":"object","properties":{"source_id":{"type":"string","description":"Optional spine.sources id to scope matching to one uploaded statement."},"window_days":{"type":"number","description":"Date window (± days) for candidate receipts. Default 3."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/statements/matches","method":"GET","description":"The statement↔receipt match adjudication queue: proposed (or signed/rejected) fact_links with both facts' display fields (statement descriptor/amount/date, receipt merchant/amount/date) + confidence, for a human to sign or reject.","inputSchema":{"type":"object","properties":{"status":{"type":"string","description":"Filter: proposed (default), signed, or rejected."},"limit":{"type":"number","description":"Page size (default 50, max 200)."},"offset":{"type":"number","description":"Page offset."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/statements/matches/:link_id/sign","method":"POST","description":"Human sign — promote a proposed statement↔receipt match to signed so downstream read models treat the pair as ONE economic event (spend counts once). Guardrailed: a fact can be in at most one signed match (409/FACT_ALREADY_MATCHED otherwise); idempotent on re-sign.","inputSchema":{"type":"object","properties":{"relation":{"type":"string","description":"matches (default), supersedes, or duplicate_of."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/statements/matches/:link_id/reject","method":"POST","description":"Dismiss a proposed (or signed) statement↔receipt match link → status='rejected'.","inputSchema":{"type":"object","properties":{}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/statements/matches/:link_id/unsign","method":"POST","description":"Reverse a signed statement↔receipt match back to proposed, freeing both facts for re-adjudication.","inputSchema":{"type":"object","properties":{}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/economic-indicators","method":"GET","description":"List economic indicators (CPI, inflation rates) for indexed loan calculations. Filter by type and date range.","inputSchema":{"type":"object","properties":{"type":{"type":"string","description":"Indicator type (e.g. 'CPI')."},"start_date":{"type":"string","description":"Filter from date (YYYY-MM-DD)."},"end_date":{"type":"string","description":"Filter to date (YYYY-MM-DD)."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/budgeting/kronan/orders/sync","method":"POST","description":"Manually import your family's completed Krónan orders as pending budgeting receipts (family admin). Idempotent — orders already imported are skipped. Until the family's backfill is complete, each run resumes paging where the last one stopped and walks to the oldest order; after that, runs import only new ones. Each run has a time budget (timedOut=true means call again to continue). Orders that fail to import are retried first on every later run, up to 5 attempts, then given up (failuresRetried/failuresRecovered/failuresGivenUp report this). Set force_backfill=true to reset the backfill and re-walk every page (no duplicates are created); it also gives given-up orders a fresh set of retries. Normally runs nightly via cron.","inputSchema":{"type":"object","properties":{"force_backfill":{"type":"boolean","description":"Reset this family's backfill marker and cursor and re-walk all Krónan order pages, and reset the retry count of failed (including given-up) orders. Already-imported orders are skipped. Family admin parents only."}}},"outputSchema":{"type":"object","properties":{"familiesProcessed":{"type":"number"},"ordersScanned":{"type":"number"},"ordersImported":{"type":"number"},"ordersSkipped":{"type":"number"},"failures":{"type":"array","items":{"type":"string"}},"timedOut":{"type":"boolean"},"familiesDeferred":{"type":"number"},"backfillsCompleted":{"type":"number"},"backfillsPending":{"type":"number"},"failuresRetried":{"type":"number"},"failuresRecovered":{"type":"number"},"failuresGivenUp":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"accessLevel":"public","tier":"write","app":"budgeting"},{"path":"/api/v1/budgeting/kronan/orders/status","method":"GET","description":"Summary of your family's imported Krónan orders: total imported, last import time, the most recent imports (token, order total, order date, linked receipt id), backfill state (complete flag, resume offset, completion time, last sync run), and failed orders (count still being retried, plus the orders given up after the retry limit with their last error — a forced backfill retries those).","inputSchema":{"type":"object","properties":{}},"outputSchema":{"type":"object","properties":{"total_imported":{"type":"number"},"last_imported_at":{"type":["string","null"]},"recent":{"type":"array","items":{"type":"object","properties":{"kronan_order_token":{"type":"string"},"order_total":{"type":"number"},"ordered_at":{"type":"string"},"imported_at":{"type":"string"},"variable_cost_id":{"type":["string","null"]}}}},"backfill_complete":{"type":"boolean"},"backfill_next_offset":{"type":"number"},"backfill_completed_at":{"type":["string","null"]},"last_run_at":{"type":["string","null"]},"failed_orders_pending":{"type":"number"},"failed_orders_given_up":{"type":"array","items":{"type":"object","properties":{"kronan_order_token":{"type":"string"},"attempts":{"type":"number"},"last_error":{"type":["string","null"]},"first_failed_at":{"type":"string"},"last_attempt_at":{"type":"string"},"given_up_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"budgeting"},{"path":"/api/v1/cpe/activities","method":"GET","description":"List CPE activities with optional filters. Returns activities with hours, provider, date, and status.","inputSchema":{"type":"object","properties":{"after":{"type":"string","description":"Filter activities on or after this date (YYYY-MM-DD)."},"before":{"type":"string","description":"Filter activities on or before this date (YYYY-MM-DD)."},"requirement_id":{"type":"string","description":"Filter by linked requirement."},"category_id":{"type":"string","description":"Filter by activity category."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/cpe/activities/create","method":"POST","description":"Log a new CPE activity with one primary category and optional requirement mappings.","inputSchema":{"type":"object","required":["title","date","hours","activity_category_id","activity_status_id"],"properties":{"title":{"type":"string","description":"Activity title."},"date":{"type":"string","description":"Activity date (YYYY-MM-DD)."},"hours":{"type":"number","description":"CPE hours earned (must be > 0)."},"provider":{"type":"string","description":"Training provider or organization."},"notes":{"type":"string","description":"Additional notes."},"proof_url":{"type":"string","description":"URL to proof/certificate."},"activity_category_id":{"type":"string","description":"Primary activity category ID."},"category_ids":{"type":"array","items":{"type":"string"},"description":"Deprecated compatibility alias; only the first category is used."},"requirement_ids":{"type":"array","items":{"type":"string"},"description":"Requirement IDs to link."},"activity_status_id":{"type":"string","description":"Status ID (e.g., completed, registered)."},"end_date":{"type":"string","description":"Optional final day (YYYY-MM-DD)."},"starts_at":{"type":"string","description":"Optional default start time."},"ends_at":{"type":"string","description":"Optional default end time."},"day_schedule":{"type":"array","description":"Optional per-day schedule for multi-day activities."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"cpe"},{"path":"/api/v1/cpe/activities/:activity_id","method":"GET","description":"Get a CPE activity with its categories and linked requirements.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/cpe/activities/:activity_id","method":"PATCH","description":"Update a CPE activity.","inputSchema":{"type":"object","properties":{"title":{"type":"string"},"date":{"type":"string"},"hours":{"type":"number"},"provider":{"type":"string"},"notes":{"type":"string"},"proof_url":{"type":"string"},"activity_status_id":{"type":"string"},"activity_category_id":{"type":"string"},"requirement_ids":{"type":"array","items":{"type":"string"}},"end_date":{"type":"string"},"starts_at":{"type":"string"},"ends_at":{"type":"string"},"day_schedule":{"type":"array"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"cpe"},{"path":"/api/v1/cpe/activities/:activity_id","method":"DELETE","description":"Delete a CPE activity and its category/requirement links.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"cpe"},{"path":"/api/v1/cpe/requirements","method":"GET","description":"List CPE requirements with progress (completed_hours, registered_hours vs required_hours). Filter by status.","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","active","completed","expired"],"description":"Filter by requirement status."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/cpe/requirements/create","method":"POST","description":"Create a CPE requirement with certification, date range, required hours, and optional per-category components.","inputSchema":{"type":"object","required":["certification_id","date_from","date_to","required_hours"],"properties":{"certification_id":{"type":"string","description":"Certification this requirement is for."},"date_from":{"type":"string","description":"Period start (YYYY-MM-DD)."},"date_to":{"type":"string","description":"Period end (YYYY-MM-DD)."},"required_hours":{"type":"number","description":"Total hours required."},"status":{"type":"string","enum":["draft","active","completed","expired"]},"components":{"type":"array","items":{"type":"object","required":["activity_category_id","required_hours"],"properties":{"activity_category_id":{"type":"string"},"required_hours":{"type":"number"}}},"description":"Per-category hour breakdown."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"cpe"},{"path":"/api/v1/cpe/requirements/:requirement_id","method":"GET","description":"Get a CPE requirement with components, progress breakdown by category, and aggregated hours.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/cpe/requirements/:requirement_id","method":"PATCH","description":"Update a CPE requirement.","inputSchema":{"type":"object","properties":{"date_from":{"type":"string"},"date_to":{"type":"string"},"required_hours":{"type":"number"},"status":{"type":"string","enum":["draft","active","completed","expired"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"cpe"},{"path":"/api/v1/cpe/requirements/:requirement_id","method":"DELETE","description":"Delete a CPE requirement and its components.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"cpe"},{"path":"/api/v1/cpe/certifications","method":"GET","description":"List available CPE certifications (e.g., CPA, CISA).","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/cpe/categories","method":"GET","description":"List CPE activity categories (e.g., Ethics, Technical, Business).","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/cpe/dashboard","method":"GET","description":"Get CPE progress dashboard: active requirements with hours breakdown (completed/registered/required, % complete, per-category components), recent activities, upcoming deadlines, total hours this year.","outputSchema":{"type":"object","properties":{"progress":{"type":"array","description":"Active requirements with hours and component breakdown."},"recent_activities":{"type":"array","description":"Last 10 activities."},"upcoming_deadlines":{"type":"array","description":"Requirements ending within 90 days with days_remaining."},"total_hours_this_year":{"type":"number"},"default_requirement_id":{"type":"string"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/cpe/settings","method":"GET","description":"Get CPE user preferences (default requirement).","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/cpe/settings","method":"PATCH","description":"Update CPE user preferences.","inputSchema":{"type":"object","properties":{"default_requirement_id":{"type":"string","description":"Set the default active requirement for dashboard."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"cpe"},{"path":"/api/v1/cpe/statuses","method":"GET","description":"List CPE activity statuses (e.g., Registered, Completed, Planned) with display order.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"cpe"},{"path":"/api/v1/wardrobe/items","method":"GET","description":"List wardrobe items with optional filters. Returns only published items by default (use status='draft' to see items awaiting metadata). Excludes archived items by default. Each item with an image includes image_path (relative storage path) and image_signed_url (time-limited URL valid for 1 hour).","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","published"],"description":"Filter by item status. Defaults to published. Use 'draft' to see items awaiting metadata."},"category":{"type":"string","enum":["tops","bottoms","outerwear","shoes","accessories","activewear"],"description":"Filter by item category."},"subcategory":{"type":"string","description":"Filter by subcategory (e.g. polo, jeans, blazer)."},"occasion":{"type":"string","enum":["work","casual","golf","formal","date_night","travel"],"description":"Filter by occasion tag."},"season":{"type":"string","enum":["spring","summer","autumn","winter"],"description":"Filter by season tag."},"condition":{"type":"string","enum":["new","good","fair","worn","retired"],"description":"Filter by condition."},"palette_score":{"type":"number","description":"Filter by exact palette score (1-5)."},"search":{"type":"string","description":"Search by item name or brand (case-insensitive)."},"include_archived":{"type":"boolean","description":"Include archived items. Defaults to false."},"sort":{"type":"string","enum":["name","purchase_date","created_at"],"description":"Sort field. Defaults to created_at."},"order":{"type":"string","enum":["asc","desc"],"description":"Sort direction. Defaults to desc."}}},"outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/items","method":"POST","description":"Create a new wardrobe item. Provide name and category at minimum. Optionally include image_base64 to upload a photo in the same call — the image will be uploaded to Supabase Storage and image_path will be populated automatically.","inputSchema":{"type":"object","required":["name","category"],"properties":{"name":{"type":"string","description":"Item name (e.g. 'Navy Lacoste Polo')."},"category":{"type":"string","enum":["tops","bottoms","outerwear","shoes","accessories","activewear"]},"subcategory":{"type":"string","description":"Specific type within category."},"brand":{"type":"string"},"size":{"type":"string"},"colors":{"type":"array","items":{"type":"string"},"description":"Array of color names."},"color_hex":{"type":"array","items":{"type":"string"},"description":"Optional hex codes matching colors array."},"palette_score":{"type":"number","description":"1-5 rating of how well item fits user's color season."},"purchase_price":{"type":"number"},"purchase_date":{"type":"string","description":"YYYY-MM-DD format."},"purchase_currency":{"type":"string","description":"Defaults to ISK."},"condition":{"type":"string","enum":["new","good","fair","worn","retired"],"description":"Defaults to good."},"seasons":{"type":"array","items":{"type":"string"}},"occasions":{"type":"array","items":{"type":"string"}},"image_path":{"type":"string","description":"Relative storage path in the wardrobe-images private bucket. Prefer image_base64 for new uploads."},"image_base64":{"type":"string","description":"Base64-encoded image data. When provided, the image is uploaded to storage and image_path is set automatically."},"content_type":{"type":"string","enum":["image/jpeg","image/png","image/webp"],"description":"MIME type of image_base64. Defaults to image/jpeg."},"notes":{"type":"string"},"status":{"type":"string","enum":["draft","published"],"description":"Item status. Defaults to published. Use 'draft' for image-only items awaiting metadata."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/items/:item_id","method":"GET","description":"Get a single wardrobe item by ID, including wear statistics (wear count, last worn, cost per wear).","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/items/:item_id","method":"PATCH","description":"Update a wardrobe item. Provide only the fields to change. Optionally include image_base64 to upload or replace a photo in the same call.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string","enum":["tops","bottoms","outerwear","shoes","accessories","activewear"]},"subcategory":{"type":"string"},"brand":{"type":"string"},"size":{"type":"string"},"colors":{"type":"array","items":{"type":"string"}},"color_hex":{"type":"array","items":{"type":"string"}},"palette_score":{"type":"number"},"purchase_price":{"type":"number"},"purchase_date":{"type":"string"},"purchase_currency":{"type":"string"},"condition":{"type":"string","enum":["new","good","fair","worn","retired"]},"seasons":{"type":"array","items":{"type":"string"}},"occasions":{"type":"array","items":{"type":"string"}},"image_path":{"type":"string"},"image_base64":{"type":"string","description":"Base64-encoded image data. When provided, the image is uploaded to storage and image_path is set automatically."},"content_type":{"type":"string","enum":["image/jpeg","image/png","image/webp"],"description":"MIME type of image_base64. Defaults to image/jpeg."},"notes":{"type":"string"},"is_archived":{"type":"boolean"},"status":{"type":"string","enum":["draft","published"],"description":"Item status. Use to promote drafts or revert to draft."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/items/:item_id","method":"DELETE","description":"Permanently delete a wardrobe item. Consider archiving (PATCH is_archived: true) instead.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/pending","method":"GET","description":"List pending wardrobe uploads awaiting cataloging. Returns uploads from the shared pending_uploads system filtered to wardrobe, with nested file metadata. Use this to find items awaiting review in the upload inbox.","outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/pending/:item_id/promote","method":"POST","description":"Promote a pending wardrobe upload to a published wardrobe item by providing full metadata. The upload_id is passed as :item_id. Requires name and category at minimum. Creates the wardrobe item, moves the uploaded image to its final storage path, and marks the upload as completed.","inputSchema":{"type":"object","required":["name","category"],"properties":{"name":{"type":"string","description":"Item name (e.g. 'Navy Lacoste Polo')."},"category":{"type":"string","enum":["tops","bottoms","outerwear","shoes","accessories","activewear"]},"subcategory":{"type":"string","description":"Specific type within category."},"brand":{"type":"string"},"size":{"type":"string"},"colors":{"type":"array","items":{"type":"string"},"description":"Array of color names."},"color_hex":{"type":"array","items":{"type":"string"},"description":"Optional hex codes matching colors array."},"palette_score":{"type":"number","description":"1-5 rating of how well item fits user's color season."},"purchase_price":{"type":"number"},"purchase_date":{"type":"string","description":"YYYY-MM-DD format."},"purchase_currency":{"type":"string","description":"Defaults to ISK."},"condition":{"type":"string","enum":["new","good","fair","worn","retired"],"description":"Defaults to good."},"seasons":{"type":"array","items":{"type":"string"}},"occasions":{"type":"array","items":{"type":"string"}},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/settings","method":"GET","description":"Get the user's wardrobe profile including their 16-season color analysis type.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"season_type":{"type":"string","description":"Color season ID, e.g. 'soft_autumn', 'true_winter'."},"season_notes":{"type":"string"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/settings","method":"PATCH","description":"Create or update the user's wardrobe profile (season type and analysis notes). Creates if not exists.","inputSchema":{"type":"object","properties":{"season_type":{"type":"string","description":"16-season type ID: true_spring, light_spring, bright_spring, warm_spring, true_summer, light_summer, soft_summer, cool_summer, true_autumn, soft_autumn, dark_autumn, warm_autumn, true_winter, dark_winter, bright_winter, cool_winter."},"season_notes":{"type":"string","description":"Free-form notes about the user's color analysis."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/outfits","method":"GET","description":"List outfits with nested item details. Filter by occasion, season, or minimum rating.","inputSchema":{"type":"object","properties":{"occasion":{"type":"string","enum":["work","casual","golf","formal","date_night","travel"]},"season":{"type":"string","enum":["spring","summer","autumn","winter"]},"min_rating":{"type":"number","description":"Minimum rating (1-5)."},"sort":{"type":"string","enum":["name","created_at","rating"],"description":"Defaults to created_at."},"order":{"type":"string","enum":["asc","desc"],"description":"Defaults to desc."}}},"outputSchema":{"type":"object","properties":{"outfits":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/outfits","method":"POST","description":"Create an outfit with associated items. Provide item_ids to link wardrobe items to this outfit.","inputSchema":{"type":"object","required":["name","item_ids"],"properties":{"name":{"type":"string","description":"Outfit name."},"item_ids":{"type":"array","items":{"type":"string"},"description":"Array of wardrobe item UUIDs to include."},"occasions":{"type":"array","items":{"type":"string"}},"seasons":{"type":"array","items":{"type":"string"}},"rating":{"type":"number","description":"1-5 rating."},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/outfits/:outfit_id","method":"GET","description":"Get a single outfit with full item details and recent wear history.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/outfits/:outfit_id","method":"PATCH","description":"Update outfit metadata and optionally replace its items. If item_ids is provided, all existing items are replaced.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"occasions":{"type":"array","items":{"type":"string"}},"seasons":{"type":"array","items":{"type":"string"}},"rating":{"type":"number"},"notes":{"type":"string"},"item_ids":{"type":"array","items":{"type":"string"},"description":"If provided, replaces all outfit items."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/outfits/:outfit_id","method":"DELETE","description":"Delete an outfit and all its item associations.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/wear","method":"GET","description":"List wear log entries (newest first) with nested item and outfit details. Filter by month or occasion.","inputSchema":{"type":"object","properties":{"month":{"type":"string","description":"Filter by month (YYYY-MM format)."},"occasion":{"type":"string","enum":["work","casual","golf","formal","date_night","travel"]},"limit":{"type":"number","description":"Max entries to return. Defaults to 50."}}},"outputSchema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/wear","method":"POST","description":"Log what you wore. Provide item_ids for the individual items, and optionally outfit_id if it's a saved outfit. Use worn_date to backfill past entries.","inputSchema":{"type":"object","required":["item_ids"],"properties":{"item_ids":{"type":"array","items":{"type":"string"},"description":"Array of wardrobe item UUIDs that were worn."},"outfit_id":{"type":"string","description":"Optional outfit UUID if logging a saved outfit."},"worn_date":{"type":"string","description":"Date worn (YYYY-MM-DD). Defaults to today."},"occasion":{"type":"string"},"weather_note":{"type":"string"},"rating":{"type":"number","description":"1-5 how the outfit felt."},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/wear/:wear_id","method":"DELETE","description":"Delete a wear log entry and its item associations.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/wishlist","method":"GET","description":"List wishlist items. Excludes purchased items by default.","inputSchema":{"type":"object","properties":{"priority":{"type":"string","enum":["low","medium","high"],"description":"Filter by priority."},"category":{"type":"string","description":"Filter by category."},"show_purchased":{"type":"boolean","description":"Include purchased items. Defaults to false."}}},"outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/wishlist","method":"POST","description":"Add an item to the wishlist.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"category":{"type":"string"},"brand":{"type":"string"},"estimated_price":{"type":"number"},"currency":{"type":"string","description":"Defaults to ISK."},"url":{"type":"string","description":"Product page URL."},"image_path":{"type":"string"},"palette_score":{"type":"number","description":"1-5 palette fit."},"priority":{"type":"string","enum":["low","medium","high"],"description":"Defaults to medium."},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/wishlist/:wishlist_id","method":"PATCH","description":"Update a wishlist item. Set is_purchased=true and purchased_item_id to mark as bought.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string"},"brand":{"type":"string"},"estimated_price":{"type":"number"},"currency":{"type":"string"},"url":{"type":"string"},"image_path":{"type":"string"},"palette_score":{"type":"number"},"priority":{"type":"string","enum":["low","medium","high"]},"notes":{"type":"string"},"is_purchased":{"type":"boolean"},"purchased_item_id":{"type":"string","description":"UUID of the wardrobe item created from this wishlist item."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/wishlist/:wishlist_id","method":"DELETE","description":"Delete a wishlist item.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/packing","method":"GET","description":"List all packing lists.","outputSchema":{"type":"object","properties":{"packing_lists":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/packing","method":"POST","description":"Create a new packing list for a trip.","inputSchema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Trip name."},"destination":{"type":"string"},"start_date":{"type":"string","description":"YYYY-MM-DD."},"end_date":{"type":"string","description":"YYYY-MM-DD."},"climate":{"type":"string","enum":["warm","cold","mixed"]},"activities":{"type":"array","items":{"type":"string"},"description":"Planned activities."},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/packing/:packing_list_id","method":"GET","description":"Get a packing list with all items and their packed status.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/packing/:packing_list_id","method":"PATCH","description":"Update packing list metadata.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"destination":{"type":"string"},"start_date":{"type":"string"},"end_date":{"type":"string"},"climate":{"type":"string","enum":["warm","cold","mixed"]},"activities":{"type":"array","items":{"type":"string"}},"notes":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/packing/:packing_list_id","method":"DELETE","description":"Delete a packing list and all its item associations.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/packing/:packing_list_id/items","method":"POST","description":"Add items to a packing list.","inputSchema":{"type":"object","required":["item_ids"],"properties":{"item_ids":{"type":"array","items":{"type":"string"},"description":"Array of wardrobe item UUIDs to add."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/packing/:packing_list_id/items","method":"DELETE","description":"Remove items from a packing list.","inputSchema":{"type":"object","required":["item_ids"],"properties":{"item_ids":{"type":"array","items":{"type":"string"},"description":"Array of wardrobe item UUIDs to remove."}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/packing/:packing_list_id/items/:item_id","method":"PATCH","description":"Toggle packed status for an item in a packing list.","inputSchema":{"type":"object","required":["is_packed"],"properties":{"is_packed":{"type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-sessions","method":"GET","description":"List color analysis sessions for the user. Returns sessions with drape counts, most recent first. Use to see session history and find sessions to review or continue.","inputSchema":{"type":"object","properties":{"status":{"type":"string","enum":["pending","in_progress","completed","needs_retake"],"description":"Filter by session status."},"session_type":{"type":"string","enum":["full","refinement","validation"],"description":"Filter by session type."}}},"outputSchema":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-sessions","method":"POST","description":"Create a new color analysis session. Defaults to status='pending' and confidence='provisional'. For refinement sessions, link to the previous session via previous_session_id.","inputSchema":{"type":"object","required":["session_type"],"properties":{"session_type":{"type":"string","enum":["full","refinement","validation"],"description":"Type of analysis session."},"previous_session_id":{"type":"string","description":"UUID of a previous session this one refines (for refinement type)."},"lighting_notes":{"type":"string","description":"Notes about lighting conditions during the session."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-sessions/:session_id","method":"GET","description":"Get a single color analysis session with all its drapes joined (ordered by sort_order). This is the primary tool for reviewing a full session — returns the session metadata plus every drape assessment.","outputSchema":{"type":"object","properties":{"id":{"type":"string"},"session_type":{"type":"string"},"status":{"type":"string"},"undertone":{"type":["string","null"]},"value_level":{"type":["string","null"]},"chroma":{"type":["string","null"]},"season_result":{"type":["string","null"]},"confidence":{"type":"string"},"drapes":{"type":"array","items":{"type":"object"}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-sessions/:session_id","method":"PATCH","description":"Update a color analysis session. Use to record the three-axis determination (undertone, value_level, chroma), season_result, confidence, and status after analyzing drapes. Auto-sets updated_at.","inputSchema":{"type":"object","properties":{"undertone":{"type":["string","null"],"enum":["warm","cool","neutral_warm","neutral_cool","neutral",null],"description":"Hue axis result."},"value_level":{"type":["string","null"],"enum":["light","medium","deep",null],"description":"Depth axis result."},"chroma":{"type":["string","null"],"enum":["soft","clear",null],"description":"Intensity axis result."},"season_result":{"type":["string","null"],"description":"16-season type (e.g. 'soft_autumn', 'true_winter')."},"confidence":{"type":"string","enum":["provisional","moderate","high"],"description":"Confidence in the determination."},"status":{"type":"string","enum":["pending","in_progress","completed","needs_retake"],"description":"Session status."},"notes":{"type":["string","null"],"description":"Analysis notes."},"lighting_notes":{"type":["string","null"],"description":"Lighting condition notes."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-drapes","method":"POST","description":"Record a single drape assessment within a color analysis session. A drape represents one color fabric held against the subject, with AI-assessed reactions. Required: session_id, drape_category, color_name. Assessment fields (score, skin_reaction, eye_effect, etc.) can be set now or updated later.","inputSchema":{"type":"object","required":["session_id","drape_category","color_name"],"properties":{"session_id":{"type":"string","description":"UUID of the session this drape belongs to."},"drape_category":{"type":"string","enum":["undertone","value","chroma","season_confirmation","individual_color"],"description":"Which diagnostic phase this drape belongs to."},"color_name":{"type":"string","description":"Name of the drape color (e.g. 'Gold', 'Silver', 'Dusty Rose')."},"color_hex":{"type":"string","description":"Hex code of the drape color."},"color_temperature":{"type":"string","enum":["warm","cool","neutral"],"description":"Temperature classification of the color."},"image_path":{"type":"string","description":"Storage path of the draping photo."},"score":{"type":"number","description":"AI assessment score 1-10. 8-10=best, 6-7=good, 4-5=neutral, 1-3=avoid."},"skin_reaction":{"type":"string","description":"How the skin responded (e.g. 'glowing', 'sallow', 'even')."},"eye_effect":{"type":"string","description":"Effect on the eyes (e.g. 'brightened', 'sparkled', 'dulled')."},"contrast_effect":{"type":"string","description":"Effect on facial contrast."},"overall_harmony":{"type":"string","enum":["excellent","good","fair","poor","clashing"],"description":"Overall harmony rating."},"analysis_notes":{"type":"string","description":"Detailed AI analysis of this drape."},"needs_retake":{"type":"boolean","description":"Whether this drape photo needs to be retaken."},"retake_reason":{"type":"string","description":"Why a retake is needed."},"sort_order":{"type":"number","description":"Display order within the session."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-drapes/batch","method":"POST","description":"Record multiple drape assessments at once. All drapes must belong to the same session. Use when recording a full diagnostic phase (e.g. all undertone drapes). Maximum 50 drapes per batch.","inputSchema":{"type":"object","required":["drapes"],"properties":{"drapes":{"type":"array","items":{"type":"object","required":["session_id","drape_category","color_name"],"properties":{"session_id":{"type":"string"},"drape_category":{"type":"string","enum":["undertone","value","chroma","season_confirmation","individual_color"]},"color_name":{"type":"string"},"color_hex":{"type":"string"},"color_temperature":{"type":"string","enum":["warm","cool","neutral"]},"image_path":{"type":"string"},"score":{"type":"number"},"skin_reaction":{"type":"string"},"eye_effect":{"type":"string"},"contrast_effect":{"type":"string"},"overall_harmony":{"type":"string","enum":["excellent","good","fair","poor","clashing"]},"analysis_notes":{"type":"string"},"needs_retake":{"type":"boolean"},"retake_reason":{"type":"string"},"sort_order":{"type":"number"}}},"description":"Array of drape objects. All must have the same session_id."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-drapes/:drape_id","method":"PATCH","description":"Update an existing drape assessment. Use to add or revise AI assessment fields (score, skin_reaction, eye_effect, etc.) after initial creation.","inputSchema":{"type":"object","properties":{"drape_category":{"type":"string","enum":["undertone","value","chroma","season_confirmation","individual_color"]},"color_name":{"type":"string"},"color_hex":{"type":["string","null"]},"color_temperature":{"type":["string","null"],"enum":["warm","cool","neutral",null]},"image_path":{"type":["string","null"]},"score":{"type":["number","null"],"description":"AI assessment score 1-10."},"skin_reaction":{"type":["string","null"]},"eye_effect":{"type":["string","null"]},"contrast_effect":{"type":["string","null"]},"overall_harmony":{"type":["string","null"],"enum":["excellent","good","fair","poor","clashing",null]},"analysis_notes":{"type":["string","null"]},"needs_retake":{"type":"boolean"},"retake_reason":{"type":["string","null"]},"sort_order":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-sessions/:session_id/photos","method":"GET","description":"Retrieve base64-encoded drape photos for a session. Returns all drapes that have an image_path, with the photo data inline as base64. This is the KEY tool for analyzing draping photos in conversation — it lets Claude see the actual images.","outputSchema":{"type":"object","properties":{"photos":{"type":"array","items":{"type":"object","properties":{"drape_id":{"type":"string"},"color_name":{"type":"string"},"drape_category":{"type":"string"},"sort_order":{"type":"number"},"image_base64":{"type":["string","null"]},"content_type":{"type":["string","null"]}}}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-palette","method":"GET","description":"List the user's personal color palette. Colors are sorted by rating priority (best first). Use to see which colors suit the user based on draping analysis.","inputSchema":{"type":"object","properties":{"rating":{"type":"string","enum":["best","good","neutral","avoid"],"description":"Filter by rating tier."},"color_family":{"type":"string","description":"Filter by color family (e.g. earth_tones, jewel_tones, pastels, neutrals, brights, metals)."}}},"outputSchema":{"type":"object","properties":{"colors":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-palette","method":"POST","description":"Add or update a color in the personal palette. Upserts on (user_id, color_hex) — if the hex already exists, updates the existing entry. Use after draping analysis to build the user's palette.","inputSchema":{"type":"object","required":["color_name","color_hex","rating"],"properties":{"color_name":{"type":"string","description":"Name of the color (e.g. 'Dusty Rose', 'Emerald Green')."},"color_hex":{"type":"string","description":"Hex code (e.g. '#B76E79'). Stored lowercase."},"rating":{"type":"string","enum":["best","good","neutral","avoid"],"description":"How well this color suits the user."},"color_family":{"type":"string","description":"Color family (e.g. earth_tones, jewel_tones, pastels, neutrals, brights, metals)."},"source_drape_id":{"type":"string","description":"UUID of the drape that identified this color."},"source_session_id":{"type":"string","description":"UUID of the session this color came from."},"notes":{"type":"string","description":"Notes about why this color works/doesn't work."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-palette/:color_id","method":"DELETE","description":"Remove a color from the personal palette.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/color-sessions/:session_id/apply","method":"POST","description":"Apply a completed session's color analysis results to the user's wardrobe profile. Validates the session is completed and has a season_result. Updates wardrobe.profiles with season_type, undertone, value_level, chroma, and confidence. Sets superseded_by on the previously active session. Only use after a full analysis is done and confirmed.","outputSchema":{"type":"object","properties":{"profile":{"type":"object"},"applied_session_id":{"type":"string"},"superseded_session_id":{"type":["string","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/items/:item_id/image","method":"POST","description":"Upload or replace a wardrobe item photo. Accepts EITHER a publicly accessible image_url (preferred — avoids base64 size limits) OR base64-encoded image data. The image is stored in a private Supabase Storage bucket (wardrobe-images). The response includes storage_path (relative path within the bucket) and image_signed_url (a time-limited URL valid for 1 hour). Use image_signed_url for display — do not cache it beyond expiry.","inputSchema":{"type":"object","required":[],"properties":{"image_url":{"type":"string","description":"Publicly accessible URL of the image to upload. The server fetches it directly — avoids base64 size limits. Preferred over the image parameter. Supports JPEG, PNG, and WebP (max 10 MB)."},"image":{"type":"string","description":"Base64-encoded image data. May include a data URI prefix (e.g. 'data:image/jpeg;base64,...') — the prefix will be stripped automatically. Max 5 MB decoded. Use image_url instead when possible."},"content_type":{"type":"string","description":"MIME type of the image. Only needed with base64 image parameter. Supported values: 'image/jpeg', 'image/png', 'image/webp'. Defaults to 'image/jpeg'. Ignored when using image_url (detected automatically)."}}},"outputSchema":{"type":"object","properties":{"storage_path":{"type":"string","description":"Relative path within the wardrobe-images private bucket (e.g. {user_id}/{item_id}.jpg)."},"image_signed_url":{"type":["string","null"],"description":"Time-limited signed URL for displaying the image (valid for 1 hour). Do not cache beyond expiry."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/wardrobe/items/:item_id/image","method":"DELETE","description":"Remove a wardrobe item's photo from the private wardrobe-images bucket and clear the image_path.","inputSchema":{"type":"object","properties":{}},"outputSchema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation message."}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"wardrobe"},{"path":"/api/v1/calendar/events","method":"GET","description":"List calendar events for a date range. Returns both personal and family events. Recurring events are expanded into individual occurrences within the range by default. Use expand_recurring=false to get raw recurring event records instead.","inputSchema":{"type":"object","required":["start_date","end_date"],"properties":{"start_date":{"type":"string","description":"Range start date (YYYY-MM-DD)."},"end_date":{"type":"string","description":"Range end date (YYYY-MM-DD)."},"category":{"type":"string","enum":["school","sports","medical","social","travel","work","other"],"description":"Filter by event category."},"family_only":{"type":"boolean","description":"If true, only return family events (exclude personal)."},"search":{"type":"string","description":"Search in event title and description (case-insensitive)."},"expand_recurring":{"type":"boolean","description":"Expand recurring events into individual occurrences. Defaults to true."}}},"outputSchema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"calendar"},{"path":"/api/v1/calendar/today","method":"GET","description":"Get today's calendar events across all calendars, sorted by start time. Recurring events are automatically expanded. Useful for daily briefings and 'what's on today' queries.","outputSchema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object"}},"count":{"type":"number"},"date":{"type":"string"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"calendar"},{"path":"/api/v1/calendar/events/:event_id","method":"GET","description":"Get a single calendar event by ID with full details including recurrence exceptions.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"calendar"},{"path":"/api/v1/calendar/events","method":"POST","description":"Create a new calendar event. Set family_id for family events, or omit for personal events. Set is_recurring=true with recurrence_frequency to create recurring events.","inputSchema":{"type":"object","required":["title","start_date"],"properties":{"title":{"type":"string","description":"Event title."},"description":{"type":"string"},"location":{"type":"string"},"start_date":{"type":"string","description":"Start date (YYYY-MM-DD)."},"start_time":{"type":"string","description":"Start time (HH:MM). Omit for all-day events."},"end_date":{"type":"string","description":"End date (YYYY-MM-DD). Omit if same day."},"end_time":{"type":"string","description":"End time (HH:MM)."},"all_day":{"type":"boolean","description":"All-day event. Defaults to false."},"is_recurring":{"type":"boolean","description":"Whether event recurs. Defaults to false."},"recurrence_frequency":{"type":"string","enum":["daily","weekly","monthly","yearly"],"description":"Required if is_recurring=true."},"recurrence_interval":{"type":"number","description":"Repeat every N units. Defaults to 1."},"recurrence_end_date":{"type":"string","description":"When recurrence stops (YYYY-MM-DD)."},"recurrence_days_of_week":{"type":"array","items":{"type":"number"},"description":"For weekly: days (0=Sun, 1=Mon, ..., 6=Sat)."},"category":{"type":"string","enum":["school","sports","medical","social","travel","work","other"]},"color":{"type":"string","description":"Hex color override."},"family_id":{"type":"string","description":"Set to create a family event. Omit for personal."},"assigned_to":{"type":"string","description":"Family member UUID this event is for."},"reminder_minutes":{"type":"number","description":"Minutes before event to remind."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"calendar"},{"path":"/api/v1/calendar/events/:event_id","method":"PATCH","description":"Update a calendar event. Provide only the fields to change.","inputSchema":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"start_date":{"type":"string"},"start_time":{"type":"string"},"end_date":{"type":"string"},"end_time":{"type":"string"},"all_day":{"type":"boolean"},"is_recurring":{"type":"boolean"},"recurrence_frequency":{"type":"string","enum":["daily","weekly","monthly","yearly"]},"recurrence_interval":{"type":"number"},"recurrence_end_date":{"type":"string"},"recurrence_days_of_week":{"type":"array","items":{"type":"number"}},"category":{"type":"string","enum":["school","sports","medical","social","travel","work","other"]},"color":{"type":"string"},"assigned_to":{"type":"string"},"reminder_minutes":{"type":"number"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"calendar"},{"path":"/api/v1/calendar/events/:event_id","method":"DELETE","description":"Delete a calendar event. For recurring events, this deletes the entire series. Use recurrence exceptions to cancel single occurrences instead.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"calendar"},{"path":"/api/v1/calendar/events/:event_id/exceptions","method":"POST","description":"Create or update the recurrence exception for a single occurrence of a recurring event (keyed by event_id + original_date). Set is_cancelled=true to skip the occurrence, provide override fields (title, start_time, etc.) to modify it, or set replacement_date to move it to another date (it then appears on that date only). Set replacement_date=null to move it back; omit it to leave an existing move unchanged.","inputSchema":{"type":"object","required":["original_date"],"properties":{"original_date":{"type":"string","description":"The generated date of the occurrence to modify (YYYY-MM-DD) — its recurrence_original_date in calendar_events results, even if it has been moved."},"replacement_date":{"type":["string","null"],"description":"Move this occurrence to another date (YYYY-MM-DD). null (or the original_date itself) un-moves it. Omit to keep any existing move. Requires original_date to be a real occurrence of the series, and cannot be combined with is_cancelled=true."},"is_cancelled":{"type":"boolean","description":"If true, this occurrence is cancelled/hidden."},"title":{"type":"string","description":"Override title for this occurrence."},"description":{"type":"string"},"start_time":{"type":"string","description":"Override start time (HH:MM)."},"end_time":{"type":"string","description":"Override end time (HH:MM)."},"location":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"calendar"},{"path":"/api/v1/notifications/send","method":"POST","description":"Send a notification. Creates a notification record, evaluates user preferences (quiet hours, channel config, type overrides), and delivers to enabled channels. In-app delivery is always included.","inputSchema":{"type":"object","required":["source_app","notification_type","priority","title"],"properties":{"user_id":{"type":"string","description":"Target user UUID. If omitted, sends to the authenticated user."},"source_app":{"type":"string","description":"App sending the notification: tasks, fitness, meals, agent, runner, hub, cpe, notes, budgeting."},"notification_type":{"type":"string","description":"Notification type: task_due, workout_reminder, chain_complete, chain_failed, morning_briefing, digest_published, security_alert, etc."},"priority":{"type":"string","enum":["urgent","normal","low"],"description":"Delivery priority. Urgent bypasses quiet hours. Low is in-app only."},"title":{"type":"string","description":"Notification title."},"body":{"type":"string","description":"Optional body text (markdown supported)."},"action_url":{"type":"string","description":"Deep link URL to the relevant resource."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notifications"},{"path":"/api/v1/notifications","method":"GET","description":"List notifications for the authenticated user, newest first. Use unread_only=true for the notification bell dropdown.","inputSchema":{"type":"object","properties":{"unread_only":{"type":"boolean","description":"If true, return only unread notifications."},"source_app":{"type":"string","description":"Filter by source app."},"limit":{"type":"number","description":"Max results (default 20, max 100)."},"offset":{"type":"number","description":"Pagination offset (default 0)."}}},"outputSchema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"total":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"notifications"},{"path":"/api/v1/notifications/unread-count","method":"GET","description":"Get the count of unread notifications. Called frequently by the notification bell component.","outputSchema":{"type":"object","properties":{"total":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"notifications"},{"path":"/api/v1/notifications/:notification_id/read","method":"PATCH","description":"Mark a single notification as read.","annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notifications"},{"path":"/api/v1/notifications/read-all","method":"PATCH","description":"Mark all unread notifications as read. Optionally filter by source_app.","inputSchema":{"type":"object","properties":{"source_app":{"type":"string","description":"Only mark notifications from this app as read."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notifications"},{"path":"/api/v1/notifications/preferences","method":"GET","description":"Get the user's notification preferences (quiet hours, channel config, type overrides). Returns defaults if no preferences are set.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"notifications"},{"path":"/api/v1/notifications/preferences","method":"PATCH","description":"Update notification preferences. Provide only fields to change. Creates a preferences row if none exists.","inputSchema":{"type":"object","properties":{"quiet_hours_start":{"type":"string","description":"Quiet hours start time (HH:MM)."},"quiet_hours_end":{"type":"string","description":"Quiet hours end time (HH:MM)."},"quiet_hours_tz":{"type":"string","description":"IANA timezone for quiet hours."},"global_mute":{"type":"boolean","description":"Mute all notifications."},"channel_config":{"type":"object","description":"Per-channel settings: { pushover: { enabled, user_key, api_token }, ntfy: { enabled, server_url, topic }, resend: { enabled, email }, in_app: { enabled } }"},"type_overrides":{"type":"object","description":"Per-type overrides: { notification_type: { channels: [...], priority_override: '...' } }"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"notifications"},{"path":"/api/v1/uploads","method":"GET","description":"List pending uploads. Filter by app (budgeting, meals, wardrobe, cpe) and status (pending, processing, completed, failed). Returns uploads with nested file metadata. Defaults to status=pending.","inputSchema":{"type":"object","properties":{"app":{"type":"string","enum":["budgeting","meals","wardrobe","cpe"],"description":"Filter by app."},"status":{"type":"string","enum":["pending","processing","completed","failed"],"description":"Filter by status. Defaults to pending."}}},"outputSchema":{"type":"object","properties":{"uploads":{"type":"array","items":{"type":"object"}},"count":{"type":"number"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"uploads"},{"path":"/api/v1/uploads","method":"POST","description":"Create a new pending upload. Specify which app it belongs to. Optionally include the first file as base64 in the same call.","inputSchema":{"type":"object","required":["app"],"properties":{"app":{"type":"string","enum":["budgeting","meals","wardrobe","cpe"],"description":"Which app this upload is for."},"title":{"type":"string","description":"Optional label for the upload."},"notes":{"type":"string","description":"Optional context or notes."},"file_base64":{"type":"string","description":"Base64-encoded file data for the first file. Optional — you can add files later."},"file_name":{"type":"string","description":"Filename for the first file. Defaults to upload.{ext}."},"content_type":{"type":"string","enum":["image/jpeg","image/png","image/webp","image/heic","application/pdf"],"description":"MIME type of the first file. Defaults to image/jpeg."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"uploads"},{"path":"/api/v1/uploads/:upload_id","method":"GET","description":"Get a single pending upload with all file details. Set include_images=true to fetch file contents as base64.","inputSchema":{"type":"object","properties":{"include_images":{"type":"boolean","description":"When true, fetch all files as base64. Useful for previewing upload contents."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"uploads"},{"path":"/api/v1/uploads/:upload_id","method":"DELETE","description":"Delete a pending upload and its files from both the database and storage. Only works for uploads in pending or failed status.","annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"uploads"},{"path":"/api/v1/uploads/:upload_id/files","method":"POST","description":"Add a file to an existing pending upload. The sort_order auto-increments if not specified. Upload must be in pending status.","inputSchema":{"type":"object","required":["file_base64","content_type"],"properties":{"file_base64":{"type":"string","description":"Base64-encoded file data."},"file_name":{"type":"string","description":"Filename. Defaults to upload.{ext}."},"content_type":{"type":"string","enum":["image/jpeg","image/png","image/webp","image/heic","application/pdf"],"description":"MIME type of the file."},"sort_order":{"type":"number","description":"Explicit sort order. Auto-increments from the highest existing value if omitted."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"uploads"},{"path":"/api/v1/uploads/:upload_id/process","method":"POST","description":"Mark a pending upload as completed and link it to a record. Moves files from pending/ prefix to their final per-app destination. Per-app behavior: budgeting creates receipt_attachments rows, meals updates recipe image_url, wardrobe updates item image_path, cpe updates activity proof_url. Cannot process completed uploads or uploads with 0 files.","inputSchema":{"type":"object","required":["result_type","result_id"],"properties":{"result_type":{"type":"string","description":"Type of the record created from this upload: variable_cost_transaction, recipe, wardrobe_item, cpe_activity."},"result_id":{"type":"string","description":"UUID of the record this upload was processed into."},"move_files":{"type":"boolean","description":"Whether to move files from pending/ to their final path. Defaults to true."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"uploads"},{"path":"/api/v1/uploads/:upload_id/fail","method":"POST","description":"Mark a pending upload as failed. Optionally include notes explaining the failure. Cannot fail a completed upload.","inputSchema":{"type":"object","properties":{"notes":{"type":"string","description":"Optional reason for the failure."}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"uploads"},{"path":"/api/v1/economics/indicators","method":"GET","description":"List economic indicators with current values. Filter by category or activity status, or search by name.","inputSchema":{"type":"object","properties":{"category":{"type":"string","description":"Filter by category: price_index, interest_rate, exchange_rate, wage, housing, macro."},"is_active":{"type":"string","description":"Filter by active status: 'true' or 'false'. Defaults to all."},"search":{"type":"string","description":"Search indicator names (Icelandic or English)."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"economics"},{"path":"/api/v1/economics/series","method":"GET","description":"Get time-series data for one or more indicators. Returns date/value arrays per indicator.","inputSchema":{"type":"object","properties":{"slugs":{"type":"string","description":"Comma-separated indicator slugs, e.g. 'is_cpi,is_wage_index'."},"start_date":{"type":"string","description":"Start date (YYYY-MM-DD). Defaults to 1 year ago."},"end_date":{"type":"string","description":"End date (YYYY-MM-DD). Defaults to today."},"limit":{"type":"string","description":"Max data points per indicator. Defaults to 1000."}},"required":["slugs"]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"economics"},{"path":"/api/v1/economics/latest","method":"GET","description":"Get latest values for indicators with period-over-period changes (1 month, 1 year).","inputSchema":{"type":"object","properties":{"slugs":{"type":"string","description":"Comma-separated indicator slugs. If omitted, returns all active."},"categories":{"type":"string","description":"Comma-separated categories to filter. E.g. 'price_index,exchange_rate'."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"economics"},{"path":"/api/v1/economics/ingestion-status","method":"GET","description":"Health check for data ingestion. Returns last run per source, recent failures, and stale indicators.","annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"economics"},{"path":"/api/v1/hub/kronan/token-status","method":"GET","description":"Get the current status of the family Krónan API token. Returns whether a token is configured, the verified identity, and when it was last verified. Read-only — does not contact Krónan.","outputSchema":{"type":"object","properties":{"isConfigured":{"type":"boolean"},"tokenHint":{"type":["string","null"]},"lastVerifiedAt":{"type":["string","null"]},"verifiedIdentityName":{"type":["string","null"]},"verifiedIdentityType":{"type":["string","null"],"enum":["user","customer_group",null]}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"hub"},{"path":"/api/v1/hub/kronan/token-test","method":"POST","description":"Test the family Krónan API token by calling Krónan /me/. Updates last_verified_at on success. Admin-only.","outputSchema":{"type":"object","properties":{"ok":{"type":"boolean"},"identity":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["user","customer_group"]}}},"lastVerifiedAt":{"type":"string"},"error":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":true},"accessLevel":"public","tier":"write","app":"hub"},{"path":"/api/v1/health/weight-log","method":"GET","description":"List the current user's weight log entries sorted by date descending. Supports optional date range filtering (from/to) and a limit (default 90, max 365). Use this to retrieve historical weight data for a user, e.g. 'show my weight for the last 3 months' or 'what was my weight in January'.","inputSchema":{"type":"object","properties":{"from":{"type":"string","description":"Start date inclusive (YYYY-MM-DD). Filters entries on or after this date."},"to":{"type":"string","description":"End date inclusive (YYYY-MM-DD). Filters entries on or before this date."},"limit":{"type":"integer","default":90,"description":"Maximum number of entries to return (1–365). Defaults to 90."}}},"outputSchema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"UUID of the weight log entry."},"recorded_at":{"type":"string","description":"Date of the entry (YYYY-MM-DD)."},"weight_kg":{"type":"number","description":"Body weight in kilograms."},"note":{"type":["string","null"],"description":"Optional free-text note."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}},"count":{"type":"integer"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"health"},{"path":"/api/v1/health/weight-log","method":"POST","description":"Log (or update) a body weight entry. If an entry already exists for the given date, it is overwritten (upsert). Use when a user reports their weight — e.g. 'I weighed 82.5 kg this morning' or 'log my weight as 180 lbs' (convert to kg first). recorded_at defaults to today's date if omitted.","inputSchema":{"type":"object","required":["weight_kg"],"properties":{"weight_kg":{"type":"number","description":"Body weight in kilograms. Must be greater than 0 and less than 500."},"recorded_at":{"type":"string","description":"Date to record (YYYY-MM-DD). Defaults to today's date on the server if omitted. Re-posting the same date overwrites the existing entry."},"note":{"type":"string","description":"Optional free-text note (e.g. 'post-run', 'morning fasted')."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string","description":"UUID of the weight log entry."},"recorded_at":{"type":"string","description":"Date of the entry (YYYY-MM-DD)."},"weight_kg":{"type":"number","description":"Body weight in kilograms."},"note":{"type":["string","null"],"description":"Optional free-text note."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"health"},{"path":"/api/v1/health/weight-log/:id","method":"GET","description":"Retrieve a single weight log entry by its UUID. Only returns entries belonging to the authenticated user. Use when you have a specific entry ID and need the full details.","inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"UUID of the weight log entry."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string","description":"UUID of the weight log entry."},"recorded_at":{"type":"string","description":"Date of the entry (YYYY-MM-DD)."},"weight_kg":{"type":"number","description":"Body weight in kilograms."},"note":{"type":["string","null"],"description":"Optional free-text note."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"health"},{"path":"/api/v1/health/weight-log/:id","method":"PATCH","description":"Update an existing weight log entry's weight_kg and/or note. Only the authenticated user's own entries can be modified. At least one of weight_kg or note must be provided. Use to correct a previously logged weight or add/update a note.","inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"UUID of the weight log entry to update."},"weight_kg":{"type":"number","description":"Updated body weight in kilograms (must be >0 and <500)."},"note":{"type":["string","null"],"description":"Updated note, or null to clear it."}}},"outputSchema":{"type":"object","properties":{"id":{"type":"string","description":"UUID of the weight log entry."},"recorded_at":{"type":"string","description":"Date of the entry (YYYY-MM-DD)."},"weight_kg":{"type":"number","description":"Body weight in kilograms."},"note":{"type":["string","null"],"description":"Optional free-text note."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"health"},{"path":"/api/v1/health/weight-log/:id","method":"DELETE","description":"Delete a weight log entry by UUID. Only the authenticated user's own entries can be deleted. Returns { deleted: true, id } on success.","inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"UUID of the weight log entry to delete."}}},"outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"health"},{"path":"/api/v1/health/nutrition-goals","method":"GET","description":"Return the authenticated user's personal nutrition goals (daily calorie target, daily protein target, goal body weight). These goals are read by the lightweight nutrition overlay to personalise meal recommendations and track progress. If no goals have been set yet, returns an object with all targets as null — never a 404. Use when you need to display or reason about the user's targets, e.g. 'what is my daily calorie goal?' or 'how much protein should I eat per day?'.","inputSchema":{"type":"object","properties":{}},"outputSchema":{"type":"object","properties":{"user_id":{"type":"string","description":"UUID of the authenticated user."},"daily_calorie_target":{"type":["integer","null"],"description":"Daily calorie target in kcal (800–6000), or null if not set."},"daily_protein_g_target":{"type":["integer","null"],"description":"Daily protein target in grams (20–400), or null if not set."},"goal_weight_kg":{"type":["number","null"],"description":"Target body weight in kilograms (>0 and <500), or null if not set."},"created_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"health"},{"path":"/api/v1/health/nutrition-goals","method":"PATCH","description":"Upsert the authenticated user's personal nutrition goals. The lightweight nutrition overlay reads these goals to personalise meal recommendations and calorie/protein tracking. All fields are optional — omitted fields are left unchanged, while explicit null clears the field. If the user has no goals row yet, one is created. Use when a user sets or updates their targets, e.g. 'set my daily protein target to 150g', 'update my calorie goal to 2200', or 'clear my goal weight'.","inputSchema":{"type":"object","properties":{"daily_calorie_target":{"type":["integer","null"],"description":"Daily calorie target in kcal. Must be between 800 and 6000, or null to clear. Omit to leave unchanged."},"daily_protein_g_target":{"type":["integer","null"],"description":"Daily protein target in grams. Must be between 20 and 400, or null to clear. Omit to leave unchanged."},"goal_weight_kg":{"type":["number","null"],"description":"Target body weight in kilograms. Must be greater than 0 and less than 500, or null to clear. Omit to leave unchanged."}}},"outputSchema":{"type":"object","properties":{"user_id":{"type":"string","description":"UUID of the authenticated user."},"daily_calorie_target":{"type":["integer","null"],"description":"Daily calorie target in kcal (800–6000), or null if not set."},"daily_protein_g_target":{"type":["integer","null"],"description":"Daily protein target in grams (20–400), or null if not set."},"goal_weight_kg":{"type":["number","null"],"description":"Target body weight in kilograms (>0 and <500), or null if not set."},"created_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"health"},{"path":"/api/v1/health/weight-log/summary","method":"GET","description":"Return a computed weight summary for the authenticated user covering the last 90 days. Includes: the most recent entry, a 7-day rolling average (null if fewer than 3 entries in the window), a 30-day delta (latest minus avg of entries from 28–32 days ago; null if no baseline data), a trend direction computed via linear regression over the last 14 days ('up', 'down', 'flat', or null if fewer than 4 entries), and a total 90-day entry count. Use for quick weight progress overviews, e.g. 'how is my weight trending?' or 'am I losing weight?'.","inputSchema":{"type":"object","properties":{}},"outputSchema":{"type":"object","properties":{"latest":{"description":"Most recent weight log entry, or null if no entries exist. Falls back to the newest entry older than the 90-day window when the window is empty — check recorded_at for its age.","oneOf":[{"type":"null"},{"type":"object","properties":{"id":{"type":"string","description":"UUID of the weight log entry."},"recorded_at":{"type":"string","description":"Date of the entry (YYYY-MM-DD)."},"weight_kg":{"type":"number","description":"Body weight in kilograms."},"note":{"type":["string","null"],"description":"Optional free-text note."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}]},"seven_day_avg":{"type":["number","null"],"description":"Average weight_kg over the last 7-day window. Null if fewer than 3 entries exist in the window."},"thirty_day_delta_kg":{"type":["number","null"],"description":"Difference between latest weight and the average weight from 28–32 days ago. Positive means heavier, negative means lighter. Null if no entries in the baseline window."},"trend":{"type":["string","null"],"enum":["down","flat","up",null],"description":"Trend direction from a linear regression over the last 14 days. 'up'/'down' = slope > 0.05 kg/week. Null if fewer than 4 entries in the window."},"entry_count":{"type":"integer","description":"Total number of weight log entries in the last 90 days."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"health"},{"path":"/api/v1/orka/periods","method":"GET","description":"List the term-versioned weekly timetable for a subject, ordered by weekday then start time. Multiple rows can share the same period_key across term versions (valid_from/valid_to) — this returns every version, not just the current one. Use to display or edit a subject's Stundatafla (schedule).","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string","description":"UUID of the subject."}}},"outputSchema":{"type":"object","properties":{"periods":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"period_key":{"type":"string","description":"Stable slug (e.g. ithrottir) carried across term versions — never rename, insert a new version instead."},"weekday":{"type":"integer","description":"ISO weekday, 1=Monday … 5=Friday."},"starts_at":{"type":"string","description":"Time of day, e.g. '08:30:00'."},"ends_at":{"type":"string"},"label":{"type":"string","description":"Display label for this period version, e.g. 'Íþróttir'."},"sort_order":{"type":"integer"},"valid_from":{"type":"string","description":"Date (YYYY-MM-DD) this version of the period became active."},"valid_to":{"type":["string","null"],"description":"Date this version stopped being active, or null if it's the current version."},"created_at":{"type":"string"},"updated_at":{"type":"string"},"day_part_id":{"type":["string","null"],"description":"Day part every observation in this slot is filed under (see orka_day_parts). Null on rows not yet converted to the observation model."},"planned_activity_mode":{"type":["string","null"],"enum":["single","one_of","unknown",null],"description":"What the slot plans: single = planned_activity_id; one_of = one of a candidate set; unknown = 'kemur í ljós'. Null = no plan yet."},"planned_activity_id":{"type":["string","null"],"description":"The planned activity when planned_activity_mode = single."}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/periods","method":"POST","description":"Add a timetable period version for a subject. Only a subject owner may write here — this is the term template, and a stray edit silently re-labels every entry that resolves to it. To start a new term version of an existing slot, close the current row (PATCH its valid_to) and insert a new one rather than editing times in place.","inputSchema":{"type":"object","required":["subject_id","period_key","weekday","starts_at","ends_at","label","valid_from"],"properties":{"subject_id":{"type":"string"},"period_key":{"type":"string","description":"Stable slug carried across term versions, e.g. 'ithrottir'."},"weekday":{"type":"integer","description":"ISO weekday, 1=Monday … 5=Friday."},"starts_at":{"type":"string","description":"Time of day, e.g. '08:30:00'. Must be before ends_at."},"ends_at":{"type":"string"},"label":{"type":"string","description":"Display label, e.g. 'Íþróttir'."},"sort_order":{"type":"integer","default":0},"valid_from":{"type":"string","description":"Date (YYYY-MM-DD) this version becomes active."},"valid_to":{"type":"string","description":"Date this version stops being active. Omit for the current, open-ended version."}}},"outputSchema":{"type":"object","properties":{"period":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"period_key":{"type":"string","description":"Stable slug (e.g. ithrottir) carried across term versions — never rename, insert a new version instead."},"weekday":{"type":"integer","description":"ISO weekday, 1=Monday … 5=Friday."},"starts_at":{"type":"string","description":"Time of day, e.g. '08:30:00'."},"ends_at":{"type":"string"},"label":{"type":"string","description":"Display label for this period version, e.g. 'Íþróttir'."},"sort_order":{"type":"integer"},"valid_from":{"type":"string","description":"Date (YYYY-MM-DD) this version of the period became active."},"valid_to":{"type":["string","null"],"description":"Date this version stopped being active, or null if it's the current version."},"created_at":{"type":"string"},"updated_at":{"type":"string"},"day_part_id":{"type":["string","null"],"description":"Day part every observation in this slot is filed under (see orka_day_parts). Null on rows not yet converted to the observation model."},"planned_activity_mode":{"type":["string","null"],"enum":["single","one_of","unknown",null],"description":"What the slot plans: single = planned_activity_id; one_of = one of a candidate set; unknown = 'kemur í ljós'. Null = no plan yet."},"planned_activity_id":{"type":["string","null"],"description":"The planned activity when planned_activity_mode = single."}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/periods/:period_id","method":"PATCH","description":"Update a timetable period's label, times, sort order, or close date (valid_to). Only a subject owner may write here. If either starts_at or ends_at is supplied, the resulting pair is re-validated to keep starts_at before ends_at.","inputSchema":{"type":"object","required":["period_id"],"properties":{"period_id":{"type":"string","description":"UUID of the period version to update."},"label":{"type":"string"},"starts_at":{"type":"string"},"ends_at":{"type":"string"},"sort_order":{"type":"integer"},"valid_to":{"type":["string","null"],"description":"Date this version stops being active, or null to reopen it."}}},"outputSchema":{"type":"object","properties":{"period":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"period_key":{"type":"string","description":"Stable slug (e.g. ithrottir) carried across term versions — never rename, insert a new version instead."},"weekday":{"type":"integer","description":"ISO weekday, 1=Monday … 5=Friday."},"starts_at":{"type":"string","description":"Time of day, e.g. '08:30:00'."},"ends_at":{"type":"string"},"label":{"type":"string","description":"Display label for this period version, e.g. 'Íþróttir'."},"sort_order":{"type":"integer"},"valid_from":{"type":"string","description":"Date (YYYY-MM-DD) this version of the period became active."},"valid_to":{"type":["string","null"],"description":"Date this version stopped being active, or null if it's the current version."},"created_at":{"type":"string"},"updated_at":{"type":"string"},"day_part_id":{"type":["string","null"],"description":"Day part every observation in this slot is filed under (see orka_day_parts). Null on rows not yet converted to the observation model."},"planned_activity_mode":{"type":["string","null"],"enum":["single","one_of","unknown",null],"description":"What the slot plans: single = planned_activity_id; one_of = one of a candidate set; unknown = 'kemur í ljós'. Null = no plan yet."},"planned_activity_id":{"type":["string","null"],"description":"The planned activity when planned_activity_mode = single."}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/periods/:period_id","method":"DELETE","description":"Delete a timetable period version. Only a subject owner may write here. Use sparingly — prefer closing a period (PATCH valid_to) over deleting, so past observations still resolve to a real period version.","inputSchema":{"type":"object","required":["period_id"],"properties":{"period_id":{"type":"string","description":"UUID of the period version to delete."}}},"outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/day-states","method":"GET","description":"List whether a subject had a school day for each date in a range — school, sick_home, holiday, field_trip, or other. Non-school days stay in the data (never deleted) so they remain visible as a confounder next to the days either side. Use to review or edit the day-state calendar.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string"},"from":{"type":"string","description":"Start date inclusive (YYYY-MM-DD)."},"to":{"type":"string","description":"End date inclusive (YYYY-MM-DD)."}}},"outputSchema":{"type":"object","properties":{"day_states":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD."},"state":{"type":"string","enum":["school","sick_home","holiday","field_trip","other"],"description":"The day's context. Scored observations remain in the overall trend; sick-at-home and holiday rows are excluded from period analysis, while field trips and other atypical school days retain their resolved periods."},"note":{"type":["string","null"]},"created_by":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/day-states","method":"POST","description":"Set the day state for a subject on a specific date — any subject member may write here. Upserts: re-posting the same subject_id + date overwrites the existing state rather than erroring. created_by is set on first insert only. Use when a user reports 'she was home sick today' or 'no school, it's a holiday'.","inputSchema":{"type":"object","required":["subject_id","date","state"],"properties":{"subject_id":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD."},"state":{"type":"string","enum":["school","sick_home","holiday","field_trip","other"],"description":"Whether there was a school day at all."},"note":{"type":"string"}}},"outputSchema":{"type":"object","properties":{"day_state":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD."},"state":{"type":"string","enum":["school","sick_home","holiday","field_trip","other"],"description":"The day's context. Scored observations remain in the overall trend; sick-at-home and holiday rows are excluded from period analysis, while field trips and other atypical school days retain their resolved periods."},"note":{"type":["string","null"]},"created_by":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/day-states/:day_state_id","method":"DELETE","description":"Delete a day-state record, reverting that date to an ordinary (implicit) school day. Any subject member may delete one. Use when a day-state was recorded in error.","inputSchema":{"type":"object","required":["day_state_id"],"properties":{"day_state_id":{"type":"string","description":"UUID of the day-state row to delete."}}},"outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/subjects","method":"GET","description":"List the subjects (children) the caller may access — either created by them or shared with them via subject membership. No filters; membership scoping happens automatically. Use to find a subject's id before calling any other orka endpoint, e.g. 'which kids am I tracking energy for'.","inputSchema":{"type":"object","properties":{}},"outputSchema":{"type":"object","properties":{"subjects":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"family_member_id":{"type":["string","null"],"description":"Optional link to the hub family child row."},"display_name":{"type":["string","null"],"description":"Override; when null the UI shows the linked family member's display_name."},"created_by":{"type":"string","description":"UUID of the user who created this subject — implicitly its owner."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/subjects/:subject_id/members","method":"GET","description":"List the members (raters) who can log for a subject, and their role (owner or logger). Use to see who is currently sharing a subject before inviting someone new, or to check a specific person's role.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string","description":"UUID of the subject."}}},"outputSchema":{"type":"object","properties":{"members":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"user_id":{"type":"string"},"role":{"type":"string","enum":["owner","logger"],"description":"owner = manages timetable/scale/members; logger = writes observations, slot occurrences, day states."},"display_name":{"type":["string","null"]},"email":{"type":["string","null"]},"created_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/subjects/:subject_id/members","method":"POST","description":"Add an existing Ralston Suite user as a member of a subject by email, so they can log observations for that child. Only a subject owner may add members. Returns 404 if no user has that email, 409 if they're already a member. Use when a user says 'add amma so she can log too' — she needs an existing Ralston Suite account first.","inputSchema":{"type":"object","required":["subject_id","email"],"properties":{"subject_id":{"type":"string"},"email":{"type":"string","description":"Email address of an existing Ralston Suite user to add."},"role":{"type":"string","enum":["owner","logger"],"default":"logger","description":"owner = can also manage the timetable, scale, and membership; logger = can log observations, context notes, slot occurrences and day states."}}},"outputSchema":{"type":"object","properties":{"member":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"user_id":{"type":"string"},"role":{"type":"string","enum":["owner","logger"],"description":"owner = manages timetable/scale/members; logger = writes observations, slot occurrences, day states."},"display_name":{"type":["string","null"]},"email":{"type":["string","null"]},"created_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/summary","method":"GET","description":"Computed Greining (analysis) summary for a child over a date range, read from the observation model (the same rows as orka_observations). One block per observer: their judgements are never averaged together, so compare observers side by side. Within an observer, repeated observations in one slot occurrence are averaged first. Activity, slot and weekday patterns use the activity that actually happened and leave out sick-at-home and holiday days, cancelled slots and pending observations (counted in pending_count / cancelled_count); the daily trend and the before/after-school delta keep every day. slot_change_rate is per slot, counted once per date whoever observed it.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string"},"from":{"type":"string","description":"Start date inclusive (YYYY-MM-DD)."},"to":{"type":"string","description":"End date inclusive (YYYY-MM-DD)."},"capture_method":{"type":"string","enum":["live","paper"],"description":"Optional filter by how the observation was captured."},"observer_id":{"type":"string","description":"Optional: only this observer's block (UUID from orka_observers)."},"observation_reason":{"type":"string","enum":["routine","state_change","all"],"description":"Optional filter by why the observation was logged. Defaults to all."}}},"outputSchema":{"type":"object","properties":{"observers":{"type":"array","items":{"type":"object","properties":{"observer_id":{"type":["string","null"],"description":"Null when the observer was not recorded."},"observer_name":{"type":["string","null"]},"n":{"type":"integer","description":"Observations in range."},"paper_count":{"type":"integer"},"by_activity":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"mean":{"type":"number"},"n":{"type":"integer"},"date_count":{"type":"integer"}}},"description":"Mean per activity that happened (key = activity id)."},"pending_count":{"type":"integer","description":"Observations left out of by_activity: activity still pending."},"cancelled_count":{"type":"integer","description":"Observations left out of by_activity: slot cancelled."},"by_slot":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"mean":{"type":"number"},"n":{"type":"integer"},"date_count":{"type":"integer"}}},"description":"Mean per timetable slot (key = period_key)."},"by_weekday":{"type":"array","description":"Mean per ISO weekday (1=Monday … 7=Sunday).","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"mean":{"type":"number"},"n":{"type":"integer"},"date_count":{"type":"integer"},"weekday":{"type":"integer"}}}},"daily_trend":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"mean":{"type":"number"},"n":{"type":"integer"}}}},"trend_change":{"type":["number","null"],"description":"Linear change first to last day; null with fewer than three days."},"before_after_school":{"type":"array","description":"Per day: mean before school (day part fyrir_skola), after school (eftir_skola) and after minus before.","items":{"type":"object","properties":{"date":{"type":"string"},"before_school":{"type":["number","null"]},"after_school":{"type":["number","null"]},"delta":{"type":["number","null"]}}}}}}},"slot_change_rate":{"type":"array","description":"How often each slot did not run as planned (cancelled, or changed to an activity outside its plan).","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"changed":{"type":"integer"},"total":{"type":"integer"},"rate":{"type":"number"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/observations","method":"GET","description":"List a subject's observations (the observation-centred model) resolved for reading: activity, planned vs actual activity of the slot and whether it changed from plan, day type (joined by date), day part, the slot's position in the day, and observer/reporter/registrar names. Every observer's rows come back side by side; never average different observers together.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string","description":"UUID of the subject (child)."},"from":{"type":"string","description":"Start date inclusive (YYYY-MM-DD), on date."},"to":{"type":"string","description":"End date inclusive (YYYY-MM-DD), on date."},"observer_id":{"type":"string","description":"Only this observer's judgements."},"reporter_id":{"type":"string"},"activity_id":{"type":"string"},"day_part_id":{"type":"string"},"period_id":{"type":"string","description":"Only observations in this slot."},"capture_method":{"type":"string","enum":["live","paper"]},"pending":{"type":"string","enum":["true","false"],"description":"true = only observations whose activity is still pending."}}},"outputSchema":{"type":"object","properties":{"observations":{"type":"array","items":{"type":"object","description":"One observation from orka.v_observations_resolved: how the child was during a span, in ONE person's judgement. Observers are separate series: read them side by side, never average them together.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":"string","description":"UTC date of observed_to (Iceland = UTC)."},"weekday":{"type":"integer","description":"ISO weekday, 1 = Monday … 7 = Sunday."},"observed_from":{"type":"string","description":"Start of the span."},"observed_to":{"type":"string","description":"End of the span: the tap time live, the slot end on paper."},"activity_id":{"type":["string","null"],"description":"What he was doing. Null only while pending."},"activity_key":{"type":["string","null"]},"activity_label":{"type":["string","null"]},"activity_pending":{"type":"boolean","description":"true while the activity of a one_of/unknown slot is not known yet. Close it by recording the slot's occurrence (orka_slot_occurrences_post)."},"scale_level_id":{"type":"string"},"scale_level_key":{"type":["string","null"]},"scale_level_label":{"type":["string","null"]},"scale_level_value":{"type":["number","string","null"],"description":"numeric_value of the level, for analysis only."},"day_state":{"type":["string","null"],"description":"The stored day state for the date (orka_day_states), if any."},"is_weekend":{"type":"boolean"},"day_type":{"type":"string","enum":["school","sick_home","holiday","field_trip","other","weekend"],"description":"Joined by date, never stored on the observation: the day state if recorded, else weekend on Sat/Sun, else school."},"day_part_id":{"type":"string"},"day_part_key":{"type":["string","null"]},"day_part_label":{"type":["string","null"]},"day_part_sort_order":{"type":["integer","null"]},"period_id":{"type":["string","null"],"description":"The timetable slot (sheet column), if any."},"period_key":{"type":["string","null"]},"period_label":{"type":["string","null"],"description":"The slot's column header."},"period_starts_at":{"type":["string","null"]},"period_ends_at":{"type":["string","null"]},"slot_position":{"type":["integer","null"],"description":"1-based position of the slot among the slots that run that date (by start time). Null outside slots."},"planned_activity_mode":{"type":["string","null"],"description":"single | one_of | unknown, or null when the slot has no plan yet."},"planned_activity_id":{"type":["string","null"]},"planned_activity_key":{"type":["string","null"]},"planned_activity_label":{"type":["string","null"]},"planned_candidate_activity_ids":{"type":["array","null"],"items":{"type":"string"},"description":"Candidate activities of a one_of slot."},"occurrence_id":{"type":["string","null"]},"occurrence_status":{"type":["string","null"],"description":"cancelled | changed, or null when nothing was recorded (the plan stood)."},"occurrence_reason":{"type":["string","null"]},"actual_activity_id":{"type":["string","null"],"description":"What actually happened in the slot: the changed occurrence's activity, else the single planned activity. Null when cancelled or unresolved."},"actual_activity_key":{"type":["string","null"]},"actual_activity_label":{"type":["string","null"]},"changed_from_plan":{"type":["boolean","null"],"description":"true when the slot was cancelled or changed outside its plan. Null outside slots or when the slot has no plan."},"observer_id":{"type":["string","null"],"description":"Whose judgement. Null = unknown."},"observer_name":{"type":["string","null"]},"observer_role":{"type":["string","null"]},"observer_assumed":{"type":"boolean","description":"true when the observer came from the default cascade and nobody confirmed it."},"reporter_id":{"type":["string","null"],"description":"Who wrote it down (e.g. the paper-sheet owner)."},"reporter_name":{"type":["string","null"]},"registered_by":{"type":["string","null"],"description":"The signed-in user who typed it in (server-set)."},"registrar_name":{"type":["string","null"]},"registered_at":{"type":"string"},"capture_method":{"type":"string","enum":["live","paper"]},"comment":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"observation_reason":{"type":"string"},"antecedent_categories":{"type":"array","items":{"type":"string"}},"observed_response":{"type":["string","null"]},"support_strategies":{"type":"array","items":{"type":"string"}},"recovery_status":{"type":"string"},"recovery_minutes":{"type":["integer","null"]},"legacy_entry_id":{"type":["string","null"],"description":"The legacy orka entry this row was converted from, if any."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/observations","method":"POST","description":"Record an observation: how the child was during a span, in one person's judgement. Only subject_id, observed_to and scale_level_id are required; everything left out is filled by the same default rules the Orka app uses (orka.observation_defaults): the slot containing observed_to, its day part, the activity (the slot occurrence's actual activity, else the slot's single planned activity, else pending for a one_of/unknown slot; outside slots the day part's default activity), 'from' (the later of the slot start and the same observer's previous 'to' that day), and the observer (activity default, else day-part default, else the caller's own observer row, flagged observer_assumed). Pass observer_id null to record an unknown observer. The registrar is always the caller. Paper sheet cells are unique per (sheet person, date, slot): re-entering one returns 409, so update it with orka_observations_update.","inputSchema":{"type":"object","required":["subject_id","observed_to","scale_level_id"],"properties":{"subject_id":{"type":"string","description":"UUID of the subject (child)."},"observed_to":{"type":"string","description":"ISO timestamp the span ends: the tap time live, or the slot end on paper."},"scale_level_id":{"type":"string","description":"UUID of the rating level (required)."},"observed_from":{"type":"string","description":"Override the default start of the span."},"period_id":{"type":"string","description":"The slot (sheet column). Default: the slot running on that date whose span contains observed_to."},"activity_id":{"type":"string","description":"Override the default activity (from the managed list)."},"day_part_id":{"type":"string","description":"Override the default day part."},"observer_id":{"type":["string","null"],"description":"Whose judgement. Omit for the default cascade; null = unknown."},"reporter_id":{"type":["string","null"],"description":"Who wrote it down (e.g. the paper-sheet owner). Null when logged directly."},"capture_method":{"type":"string","enum":["live","paper"],"default":"live","description":"paper needs a reporter or an observer."},"comment":{"type":"string","description":"Free text about him."},"tags":{"type":"array","items":{"type":"string"}},"observation_reason":{"type":"string","enum":["routine","state_change"]},"antecedent_categories":{"type":"array","items":{"type":"string","enum":["social_situation","unexpected_change","transition","sensory_load","task_demand","physical_state","unclear","other"]}},"observed_response":{"type":["string","null"]},"support_strategies":{"type":"array","items":{"type":"string","enum":["quiet_space","adult_support","time","movement","clear_explanation","sensory_support","none","other"]},"description":"none is exclusive."},"recovery_status":{"type":"string","enum":["unknown","ongoing","recovered"]},"recovery_minutes":{"type":["integer","null"],"minimum":0,"maximum":1440,"description":"Only with recovery_status = recovered."}}},"outputSchema":{"type":"object","properties":{"observation":{"type":"object","description":"One observation from orka.v_observations_resolved: how the child was during a span, in ONE person's judgement. Observers are separate series: read them side by side, never average them together.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":"string","description":"UTC date of observed_to (Iceland = UTC)."},"weekday":{"type":"integer","description":"ISO weekday, 1 = Monday … 7 = Sunday."},"observed_from":{"type":"string","description":"Start of the span."},"observed_to":{"type":"string","description":"End of the span: the tap time live, the slot end on paper."},"activity_id":{"type":["string","null"],"description":"What he was doing. Null only while pending."},"activity_key":{"type":["string","null"]},"activity_label":{"type":["string","null"]},"activity_pending":{"type":"boolean","description":"true while the activity of a one_of/unknown slot is not known yet. Close it by recording the slot's occurrence (orka_slot_occurrences_post)."},"scale_level_id":{"type":"string"},"scale_level_key":{"type":["string","null"]},"scale_level_label":{"type":["string","null"]},"scale_level_value":{"type":["number","string","null"],"description":"numeric_value of the level, for analysis only."},"day_state":{"type":["string","null"],"description":"The stored day state for the date (orka_day_states), if any."},"is_weekend":{"type":"boolean"},"day_type":{"type":"string","enum":["school","sick_home","holiday","field_trip","other","weekend"],"description":"Joined by date, never stored on the observation: the day state if recorded, else weekend on Sat/Sun, else school."},"day_part_id":{"type":"string"},"day_part_key":{"type":["string","null"]},"day_part_label":{"type":["string","null"]},"day_part_sort_order":{"type":["integer","null"]},"period_id":{"type":["string","null"],"description":"The timetable slot (sheet column), if any."},"period_key":{"type":["string","null"]},"period_label":{"type":["string","null"],"description":"The slot's column header."},"period_starts_at":{"type":["string","null"]},"period_ends_at":{"type":["string","null"]},"slot_position":{"type":["integer","null"],"description":"1-based position of the slot among the slots that run that date (by start time). Null outside slots."},"planned_activity_mode":{"type":["string","null"],"description":"single | one_of | unknown, or null when the slot has no plan yet."},"planned_activity_id":{"type":["string","null"]},"planned_activity_key":{"type":["string","null"]},"planned_activity_label":{"type":["string","null"]},"planned_candidate_activity_ids":{"type":["array","null"],"items":{"type":"string"},"description":"Candidate activities of a one_of slot."},"occurrence_id":{"type":["string","null"]},"occurrence_status":{"type":["string","null"],"description":"cancelled | changed, or null when nothing was recorded (the plan stood)."},"occurrence_reason":{"type":["string","null"]},"actual_activity_id":{"type":["string","null"],"description":"What actually happened in the slot: the changed occurrence's activity, else the single planned activity. Null when cancelled or unresolved."},"actual_activity_key":{"type":["string","null"]},"actual_activity_label":{"type":["string","null"]},"changed_from_plan":{"type":["boolean","null"],"description":"true when the slot was cancelled or changed outside its plan. Null outside slots or when the slot has no plan."},"observer_id":{"type":["string","null"],"description":"Whose judgement. Null = unknown."},"observer_name":{"type":["string","null"]},"observer_role":{"type":["string","null"]},"observer_assumed":{"type":"boolean","description":"true when the observer came from the default cascade and nobody confirmed it."},"reporter_id":{"type":["string","null"],"description":"Who wrote it down (e.g. the paper-sheet owner)."},"reporter_name":{"type":["string","null"]},"registered_by":{"type":["string","null"],"description":"The signed-in user who typed it in (server-set)."},"registrar_name":{"type":["string","null"]},"registered_at":{"type":"string"},"capture_method":{"type":"string","enum":["live","paper"]},"comment":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"observation_reason":{"type":"string"},"antecedent_categories":{"type":"array","items":{"type":"string"}},"observed_response":{"type":["string","null"]},"support_strategies":{"type":"array","items":{"type":"string"}},"recovery_status":{"type":"string"},"recovery_minutes":{"type":["integer","null"]},"legacy_entry_id":{"type":["string","null"],"description":"The legacy orka entry this row was converted from, if any."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/observations/pending","method":"GET","description":"The pending list: every observation whose activity is not known yet (a one_of or unknown slot nobody has resolved), grouped by date and slot with the slot's candidate activities. Close a group by recording what happened in that slot with orka_slot_occurrences_post (status changed + actual_activity_id); that fills in every pending observation of the slot and date.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string"},"from":{"type":"string","description":"Start date inclusive (YYYY-MM-DD)."},"to":{"type":"string","description":"End date inclusive (YYYY-MM-DD)."}}},"outputSchema":{"type":"object","properties":{"count":{"type":"integer","description":"Number of pending observations."},"slots":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"period_id":{"type":["string","null"]},"period_key":{"type":["string","null"]},"period_label":{"type":["string","null"]},"slot_position":{"type":["integer","null"]},"planned_activity_mode":{"type":["string","null"]},"planned_activity_id":{"type":["string","null"]},"planned_activity_label":{"type":["string","null"]},"planned_candidate_activity_ids":{"type":"array","items":{"type":"string"}},"observations":{"type":"array","items":{"type":"object","description":"One observation from orka.v_observations_resolved: how the child was during a span, in ONE person's judgement. Observers are separate series: read them side by side, never average them together.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":"string","description":"UTC date of observed_to (Iceland = UTC)."},"weekday":{"type":"integer","description":"ISO weekday, 1 = Monday … 7 = Sunday."},"observed_from":{"type":"string","description":"Start of the span."},"observed_to":{"type":"string","description":"End of the span: the tap time live, the slot end on paper."},"activity_id":{"type":["string","null"],"description":"What he was doing. Null only while pending."},"activity_key":{"type":["string","null"]},"activity_label":{"type":["string","null"]},"activity_pending":{"type":"boolean","description":"true while the activity of a one_of/unknown slot is not known yet. Close it by recording the slot's occurrence (orka_slot_occurrences_post)."},"scale_level_id":{"type":"string"},"scale_level_key":{"type":["string","null"]},"scale_level_label":{"type":["string","null"]},"scale_level_value":{"type":["number","string","null"],"description":"numeric_value of the level, for analysis only."},"day_state":{"type":["string","null"],"description":"The stored day state for the date (orka_day_states), if any."},"is_weekend":{"type":"boolean"},"day_type":{"type":"string","enum":["school","sick_home","holiday","field_trip","other","weekend"],"description":"Joined by date, never stored on the observation: the day state if recorded, else weekend on Sat/Sun, else school."},"day_part_id":{"type":"string"},"day_part_key":{"type":["string","null"]},"day_part_label":{"type":["string","null"]},"day_part_sort_order":{"type":["integer","null"]},"period_id":{"type":["string","null"],"description":"The timetable slot (sheet column), if any."},"period_key":{"type":["string","null"]},"period_label":{"type":["string","null"],"description":"The slot's column header."},"period_starts_at":{"type":["string","null"]},"period_ends_at":{"type":["string","null"]},"slot_position":{"type":["integer","null"],"description":"1-based position of the slot among the slots that run that date (by start time). Null outside slots."},"planned_activity_mode":{"type":["string","null"],"description":"single | one_of | unknown, or null when the slot has no plan yet."},"planned_activity_id":{"type":["string","null"]},"planned_activity_key":{"type":["string","null"]},"planned_activity_label":{"type":["string","null"]},"planned_candidate_activity_ids":{"type":["array","null"],"items":{"type":"string"},"description":"Candidate activities of a one_of slot."},"occurrence_id":{"type":["string","null"]},"occurrence_status":{"type":["string","null"],"description":"cancelled | changed, or null when nothing was recorded (the plan stood)."},"occurrence_reason":{"type":["string","null"]},"actual_activity_id":{"type":["string","null"],"description":"What actually happened in the slot: the changed occurrence's activity, else the single planned activity. Null when cancelled or unresolved."},"actual_activity_key":{"type":["string","null"]},"actual_activity_label":{"type":["string","null"]},"changed_from_plan":{"type":["boolean","null"],"description":"true when the slot was cancelled or changed outside its plan. Null outside slots or when the slot has no plan."},"observer_id":{"type":["string","null"],"description":"Whose judgement. Null = unknown."},"observer_name":{"type":["string","null"]},"observer_role":{"type":["string","null"]},"observer_assumed":{"type":"boolean","description":"true when the observer came from the default cascade and nobody confirmed it."},"reporter_id":{"type":["string","null"],"description":"Who wrote it down (e.g. the paper-sheet owner)."},"reporter_name":{"type":["string","null"]},"registered_by":{"type":["string","null"],"description":"The signed-in user who typed it in (server-set)."},"registrar_name":{"type":["string","null"]},"registered_at":{"type":"string"},"capture_method":{"type":"string","enum":["live","paper"]},"comment":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"observation_reason":{"type":"string"},"antecedent_categories":{"type":"array","items":{"type":"string"}},"observed_response":{"type":["string","null"]},"support_strategies":{"type":"array","items":{"type":"string"}},"recovery_status":{"type":"string"},"recovery_minutes":{"type":["integer","null"]},"legacy_entry_id":{"type":["string","null"],"description":"The legacy orka entry this row was converted from, if any."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/observations/defaults","method":"GET","description":"Preview what orka_observations_post would fill in for an observation ending at observed_to: slot, day part, activity or pending, default 'from', and observer with observer_assumed. Read-only; uses the same orka.observation_defaults function as the app.","inputSchema":{"type":"object","required":["subject_id","observed_to"],"properties":{"subject_id":{"type":"string"},"observed_to":{"type":"string","description":"ISO timestamp the span ends."},"period_id":{"type":"string"},"observer_id":{"type":"string"},"activity_id":{"type":"string"}}},"outputSchema":{"type":"object","properties":{"defaults":{"type":"object","properties":{"period_id":{"type":["string","null"]},"date":{"type":"string"},"day_part_id":{"type":["string","null"]},"activity_id":{"type":["string","null"]},"activity_pending":{"type":"boolean"},"observed_from":{"type":["string","null"]},"observer_id":{"type":["string","null"]},"observer_assumed":{"type":"boolean"}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/observations/:observation_id","method":"GET","description":"Read one resolved observation (same fields as orka_observations).","inputSchema":{"type":"object","required":["observation_id"],"properties":{"observation_id":{"type":"string"}}},"outputSchema":{"type":"object","properties":{"observation":{"type":"object","description":"One observation from orka.v_observations_resolved: how the child was during a span, in ONE person's judgement. Observers are separate series: read them side by side, never average them together.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":"string","description":"UTC date of observed_to (Iceland = UTC)."},"weekday":{"type":"integer","description":"ISO weekday, 1 = Monday … 7 = Sunday."},"observed_from":{"type":"string","description":"Start of the span."},"observed_to":{"type":"string","description":"End of the span: the tap time live, the slot end on paper."},"activity_id":{"type":["string","null"],"description":"What he was doing. Null only while pending."},"activity_key":{"type":["string","null"]},"activity_label":{"type":["string","null"]},"activity_pending":{"type":"boolean","description":"true while the activity of a one_of/unknown slot is not known yet. Close it by recording the slot's occurrence (orka_slot_occurrences_post)."},"scale_level_id":{"type":"string"},"scale_level_key":{"type":["string","null"]},"scale_level_label":{"type":["string","null"]},"scale_level_value":{"type":["number","string","null"],"description":"numeric_value of the level, for analysis only."},"day_state":{"type":["string","null"],"description":"The stored day state for the date (orka_day_states), if any."},"is_weekend":{"type":"boolean"},"day_type":{"type":"string","enum":["school","sick_home","holiday","field_trip","other","weekend"],"description":"Joined by date, never stored on the observation: the day state if recorded, else weekend on Sat/Sun, else school."},"day_part_id":{"type":"string"},"day_part_key":{"type":["string","null"]},"day_part_label":{"type":["string","null"]},"day_part_sort_order":{"type":["integer","null"]},"period_id":{"type":["string","null"],"description":"The timetable slot (sheet column), if any."},"period_key":{"type":["string","null"]},"period_label":{"type":["string","null"],"description":"The slot's column header."},"period_starts_at":{"type":["string","null"]},"period_ends_at":{"type":["string","null"]},"slot_position":{"type":["integer","null"],"description":"1-based position of the slot among the slots that run that date (by start time). Null outside slots."},"planned_activity_mode":{"type":["string","null"],"description":"single | one_of | unknown, or null when the slot has no plan yet."},"planned_activity_id":{"type":["string","null"]},"planned_activity_key":{"type":["string","null"]},"planned_activity_label":{"type":["string","null"]},"planned_candidate_activity_ids":{"type":["array","null"],"items":{"type":"string"},"description":"Candidate activities of a one_of slot."},"occurrence_id":{"type":["string","null"]},"occurrence_status":{"type":["string","null"],"description":"cancelled | changed, or null when nothing was recorded (the plan stood)."},"occurrence_reason":{"type":["string","null"]},"actual_activity_id":{"type":["string","null"],"description":"What actually happened in the slot: the changed occurrence's activity, else the single planned activity. Null when cancelled or unresolved."},"actual_activity_key":{"type":["string","null"]},"actual_activity_label":{"type":["string","null"]},"changed_from_plan":{"type":["boolean","null"],"description":"true when the slot was cancelled or changed outside its plan. Null outside slots or when the slot has no plan."},"observer_id":{"type":["string","null"],"description":"Whose judgement. Null = unknown."},"observer_name":{"type":["string","null"]},"observer_role":{"type":["string","null"]},"observer_assumed":{"type":"boolean","description":"true when the observer came from the default cascade and nobody confirmed it."},"reporter_id":{"type":["string","null"],"description":"Who wrote it down (e.g. the paper-sheet owner)."},"reporter_name":{"type":["string","null"]},"registered_by":{"type":["string","null"],"description":"The signed-in user who typed it in (server-set)."},"registrar_name":{"type":["string","null"]},"registered_at":{"type":"string"},"capture_method":{"type":"string","enum":["live","paper"]},"comment":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"observation_reason":{"type":"string"},"antecedent_categories":{"type":"array","items":{"type":"string"}},"observed_response":{"type":["string","null"]},"support_strategies":{"type":"array","items":{"type":"string"}},"recovery_status":{"type":"string"},"recovery_minutes":{"type":["integer","null"]},"legacy_entry_id":{"type":["string","null"],"description":"The legacy orka entry this row was converted from, if any."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/observations/:observation_id","method":"PATCH","description":"Correct an observation. Subject owners may edit any observation; other members only ones they registered or where they are the observer or the reporter (403 otherwise). Naming activity_id resolves a pending observation. Changing the observer clears observer_assumed; set observer_assumed false to confirm an assumed observer. Who registered it and when can never be changed. At least one field is required.","inputSchema":{"type":"object","required":["observation_id"],"properties":{"observation_id":{"type":"string"},"activity_id":{"type":"string"},"observed_from":{"type":"string"},"observed_to":{"type":"string"},"day_part_id":{"type":"string"},"period_id":{"type":["string","null"]},"scale_level_id":{"type":"string"},"observer_id":{"type":["string","null"]},"observer_assumed":{"type":"boolean","description":"Only false (confirm) is accepted."},"reporter_id":{"type":["string","null"]},"capture_method":{"type":"string","enum":["live","paper"]},"comment":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"observation_reason":{"type":"string","enum":["routine","state_change"]},"antecedent_categories":{"type":"array","items":{"type":"string","enum":["social_situation","unexpected_change","transition","sensory_load","task_demand","physical_state","unclear","other"]}},"observed_response":{"type":["string","null"]},"support_strategies":{"type":"array","items":{"type":"string","enum":["quiet_space","adult_support","time","movement","clear_explanation","sensory_support","none","other"]},"description":"none is exclusive."},"recovery_status":{"type":"string","enum":["unknown","ongoing","recovered"]},"recovery_minutes":{"type":["integer","null"],"minimum":0,"maximum":1440,"description":"Only with recovery_status = recovered."}}},"outputSchema":{"type":"object","properties":{"observation":{"type":"object","description":"One observation from orka.v_observations_resolved: how the child was during a span, in ONE person's judgement. Observers are separate series: read them side by side, never average them together.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":"string","description":"UTC date of observed_to (Iceland = UTC)."},"weekday":{"type":"integer","description":"ISO weekday, 1 = Monday … 7 = Sunday."},"observed_from":{"type":"string","description":"Start of the span."},"observed_to":{"type":"string","description":"End of the span: the tap time live, the slot end on paper."},"activity_id":{"type":["string","null"],"description":"What he was doing. Null only while pending."},"activity_key":{"type":["string","null"]},"activity_label":{"type":["string","null"]},"activity_pending":{"type":"boolean","description":"true while the activity of a one_of/unknown slot is not known yet. Close it by recording the slot's occurrence (orka_slot_occurrences_post)."},"scale_level_id":{"type":"string"},"scale_level_key":{"type":["string","null"]},"scale_level_label":{"type":["string","null"]},"scale_level_value":{"type":["number","string","null"],"description":"numeric_value of the level, for analysis only."},"day_state":{"type":["string","null"],"description":"The stored day state for the date (orka_day_states), if any."},"is_weekend":{"type":"boolean"},"day_type":{"type":"string","enum":["school","sick_home","holiday","field_trip","other","weekend"],"description":"Joined by date, never stored on the observation: the day state if recorded, else weekend on Sat/Sun, else school."},"day_part_id":{"type":"string"},"day_part_key":{"type":["string","null"]},"day_part_label":{"type":["string","null"]},"day_part_sort_order":{"type":["integer","null"]},"period_id":{"type":["string","null"],"description":"The timetable slot (sheet column), if any."},"period_key":{"type":["string","null"]},"period_label":{"type":["string","null"],"description":"The slot's column header."},"period_starts_at":{"type":["string","null"]},"period_ends_at":{"type":["string","null"]},"slot_position":{"type":["integer","null"],"description":"1-based position of the slot among the slots that run that date (by start time). Null outside slots."},"planned_activity_mode":{"type":["string","null"],"description":"single | one_of | unknown, or null when the slot has no plan yet."},"planned_activity_id":{"type":["string","null"]},"planned_activity_key":{"type":["string","null"]},"planned_activity_label":{"type":["string","null"]},"planned_candidate_activity_ids":{"type":["array","null"],"items":{"type":"string"},"description":"Candidate activities of a one_of slot."},"occurrence_id":{"type":["string","null"]},"occurrence_status":{"type":["string","null"],"description":"cancelled | changed, or null when nothing was recorded (the plan stood)."},"occurrence_reason":{"type":["string","null"]},"actual_activity_id":{"type":["string","null"],"description":"What actually happened in the slot: the changed occurrence's activity, else the single planned activity. Null when cancelled or unresolved."},"actual_activity_key":{"type":["string","null"]},"actual_activity_label":{"type":["string","null"]},"changed_from_plan":{"type":["boolean","null"],"description":"true when the slot was cancelled or changed outside its plan. Null outside slots or when the slot has no plan."},"observer_id":{"type":["string","null"],"description":"Whose judgement. Null = unknown."},"observer_name":{"type":["string","null"]},"observer_role":{"type":["string","null"]},"observer_assumed":{"type":"boolean","description":"true when the observer came from the default cascade and nobody confirmed it."},"reporter_id":{"type":["string","null"],"description":"Who wrote it down (e.g. the paper-sheet owner)."},"reporter_name":{"type":["string","null"]},"registered_by":{"type":["string","null"],"description":"The signed-in user who typed it in (server-set)."},"registrar_name":{"type":["string","null"]},"registered_at":{"type":"string"},"capture_method":{"type":"string","enum":["live","paper"]},"comment":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"observation_reason":{"type":"string"},"antecedent_categories":{"type":"array","items":{"type":"string"}},"observed_response":{"type":["string","null"]},"support_strategies":{"type":"array","items":{"type":"string"}},"recovery_status":{"type":"string"},"recovery_minutes":{"type":["integer","null"]},"legacy_entry_id":{"type":["string","null"],"description":"The legacy orka entry this row was converted from, if any."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/observations/:observation_id","method":"DELETE","description":"Delete an observation. Same rule as orka_observations_update: owners any, other members only ones they registered or are the observer/reporter of. The audit log keeps the deleted row. Use only when a user explicitly asks to remove one.","inputSchema":{"type":"object","required":["observation_id"],"properties":{"observation_id":{"type":"string"}}},"outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/context-notes","method":"GET","description":"List context notes for a subject: facts that are not ratings ('Er með smá kvef', 'Hjá ömmu og afa'), as a whole day or a span. Read them next to observations as possible explanations; they never count as an observation. from/to keep day notes in range and span notes overlapping it.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string"},"from":{"type":"string","description":"Start date inclusive (YYYY-MM-DD)."},"to":{"type":"string","description":"End date inclusive (YYYY-MM-DD)."}}},"outputSchema":{"type":"object","properties":{"context_notes":{"type":"array","items":{"type":"object","description":"A fact that is not a rating (a cold, where he slept). Never counted as an observation.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":["string","null"],"description":"A whole day (YYYY-MM-DD), or null for a span."},"observed_from":{"type":["string","null"]},"observed_to":{"type":["string","null"]},"kind":{"type":["string","null"],"enum":["health","sleep","location","other",null]},"text":{"type":"string"},"observer_id":{"type":["string","null"]},"reporter_id":{"type":["string","null"]},"registered_by":{"type":["string","null"]},"registered_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/context-notes","method":"POST","description":"Add a context note: a fact that is not a rating. Give either date (a whole day) or observed_from and observed_to (a span), not both. The registrar is always the caller. Use for things like 'he has a cold' or 'slept at his grandparents''.","inputSchema":{"type":"object","required":["subject_id","text"],"properties":{"subject_id":{"type":"string"},"text":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD for a whole-day note."},"observed_from":{"type":"string","description":"Span start (ISO timestamp)."},"observed_to":{"type":"string","description":"Span end (ISO timestamp)."},"kind":{"type":"string","enum":["health","sleep","location","other"]},"observer_id":{"type":["string","null"],"description":"Whose observation this is, if known."},"reporter_id":{"type":["string","null"],"description":"Who wrote it down, if not the caller."}}},"outputSchema":{"type":"object","properties":{"context_note":{"type":"object","description":"A fact that is not a rating (a cold, where he slept). Never counted as an observation.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":["string","null"],"description":"A whole day (YYYY-MM-DD), or null for a span."},"observed_from":{"type":["string","null"]},"observed_to":{"type":["string","null"]},"kind":{"type":["string","null"],"enum":["health","sleep","location","other",null]},"text":{"type":"string"},"observer_id":{"type":["string","null"]},"reporter_id":{"type":["string","null"]},"registered_by":{"type":["string","null"]},"registered_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/context-notes/:note_id","method":"PATCH","description":"Edit a context note. Same rule as observations: owners any, other members only notes they registered or are the observer/reporter of. To switch between a whole day and a span, send the new fields and null the old ones.","inputSchema":{"type":"object","required":["note_id"],"properties":{"note_id":{"type":"string"},"text":{"type":"string"},"kind":{"type":["string","null"],"enum":["health","sleep","location","other",null]},"date":{"type":["string","null"]},"observed_from":{"type":["string","null"]},"observed_to":{"type":["string","null"]},"observer_id":{"type":["string","null"]},"reporter_id":{"type":["string","null"]}}},"outputSchema":{"type":"object","properties":{"context_note":{"type":"object","description":"A fact that is not a rating (a cold, where he slept). Never counted as an observation.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"date":{"type":["string","null"],"description":"A whole day (YYYY-MM-DD), or null for a span."},"observed_from":{"type":["string","null"]},"observed_to":{"type":["string","null"]},"kind":{"type":["string","null"],"enum":["health","sleep","location","other",null]},"text":{"type":"string"},"observer_id":{"type":["string","null"]},"reporter_id":{"type":["string","null"]},"registered_by":{"type":["string","null"]},"registered_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/context-notes/:note_id","method":"DELETE","description":"Delete a context note (same edit rule as orka_context_notes_update). The audit log keeps the deleted row.","inputSchema":{"type":"object","required":["note_id"],"properties":{"note_id":{"type":"string"}}},"outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/slot-occurrences","method":"GET","description":"List what actually happened to timetable slots on given dates (cancelled, or changed to an actual activity). No row for a slot and date means the plan stood.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string"},"from":{"type":"string","description":"Start date inclusive (YYYY-MM-DD)."},"to":{"type":"string","description":"End date inclusive (YYYY-MM-DD)."},"period_id":{"type":"string"}}},"outputSchema":{"type":"object","properties":{"slot_occurrences":{"type":"array","items":{"type":"object","description":"What happened to a timetable slot on one date. No row = the plan stood.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"period_id":{"type":"string"},"date":{"type":"string"},"status":{"type":"string","enum":["cancelled","changed"]},"actual_activity_id":{"type":["string","null"]},"reason":{"type":["string","null"]},"recorded_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/slot-occurrences","method":"POST","description":"Record what happened to a slot on a date; any subject member may. status cancelled = the slot did not happen; status changed + actual_activity_id = it happened as that activity (also how a one_of/unknown slot is resolved). Recording a changed occurrence fills in the activity on every pending observation of that slot and date, in the same transaction. One row per slot and date: posting again for the same slot and date updates it.","inputSchema":{"type":"object","required":["subject_id","period_id","date","status"],"properties":{"subject_id":{"type":"string"},"period_id":{"type":"string","description":"The slot (must run on that date)."},"date":{"type":"string","description":"YYYY-MM-DD."},"status":{"type":"string","enum":["cancelled","changed"]},"actual_activity_id":{"type":"string","description":"Required when status is changed; from the subject's activity list."},"reason":{"type":["string","null"],"description":"Optional short reason, e.g. 'veður'."}}},"outputSchema":{"type":"object","properties":{"slot_occurrence":{"type":"object","description":"What happened to a timetable slot on one date. No row = the plan stood.","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"period_id":{"type":"string"},"date":{"type":"string"},"status":{"type":"string","enum":["cancelled","changed"]},"actual_activity_id":{"type":["string","null"]},"reason":{"type":["string","null"]},"recorded_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/slot-occurrences/:occurrence_id","method":"DELETE","description":"Delete a recorded slot occurrence (any subject member); the slot then reads as planned again. Observations it already filled in keep their activity.","inputSchema":{"type":"object","required":["occurrence_id"],"properties":{"occurrence_id":{"type":"string"}}},"outputSchema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"openWorldHint":false},"accessLevel":"public","tier":"write","app":"orka"},{"path":"/api/v1/orka/activities","method":"GET","description":"List a subject's managed activity list (what an observation is filed under), retired ones included unless active_only=true.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string"},"active_only":{"type":"string","enum":["true","false"]}}},"outputSchema":{"type":"object","properties":{"activities":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"key":{"type":"string","description":"Stable slug."},"label":{"type":"string","description":"Icelandic display label."},"sort_order":{"type":"integer"},"is_active":{"type":"boolean","description":"false = retired; history still resolves."},"default_observer_id":{"type":["string","null"],"description":"Step 1 of the observer cascade for observations of this activity."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/observers","method":"GET","description":"List the people whose judgement can be recorded for a subject: one per member plus people without accounts (e.g. a teacher). Retired ones included unless active_only=true.","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string"},"active_only":{"type":"string","enum":["true","false"]}}},"outputSchema":{"type":"object","properties":{"observers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"display_name":{"type":"string"},"role":{"type":["string","null"],"description":"e.g. amma, foreldri, kennari."},"member_id":{"type":["string","null"],"description":"The subject membership this observer belongs to, or null for a person without an account."},"is_active":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"},{"path":"/api/v1/orka/day-parts","method":"GET","description":"List a subject's day parts (before school, at school, after school, evening, home) with their default observer, default activity, clock window and the days it applies on (all, school or non_school days).","inputSchema":{"type":"object","required":["subject_id"],"properties":{"subject_id":{"type":"string"}}},"outputSchema":{"type":"object","properties":{"day_parts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subject_id":{"type":"string"},"key":{"type":"string"},"label":{"type":"string"},"sort_order":{"type":"integer"},"is_active":{"type":"boolean"},"default_observer_id":{"type":["string","null"],"description":"Step 2 of the observer cascade."},"default_activity_id":{"type":["string","null"],"description":"Activity of an observation outside every slot in this day part."},"window_start":{"type":"string","description":"Default clock window start, e.g. '14:00:00'."},"window_end":{"type":"string"},"days":{"type":"string","enum":["all","school","non_school"],"description":"On which days the window applies: all, school (school days) or non_school (weekends and days with a holiday / sick_home day state). A school and a non_school day part may share a window (e.g. skoli and heima)."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"openWorldHint":false},"accessLevel":"public","tier":"read","app":"orka"}]}}