diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7db3a42..838745e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,6 +103,28 @@ jobs: - name: pytest (integration) run: uv run pytest -v -m integration + client-drift: + name: client-drift + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install Python + run: uv python install 3.12 + + # Regenerate each vendored generated client from its committed OpenAPI + # snapshot and fail on any diff vs the committed generated/ tree — turns a + # stale client (the #66 failure mode) into a red build. Stdlib-only driver + # (--no-project skips the root sync); the per-SDK regen syncs that SDK's + # own lockfile, pinning its openapi-python-client + ruff versions. + - name: detect generated-client drift vs committed OpenAPI snapshot + run: uv run --no-project --python 3.12 python scripts/check_client_drift.py + changelog: name: changelog runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index fc39bcd..cbcb9cc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ htmlcov/ .worktrees/ .claude/settings.local.json node_modules/ + +# Ephemeral regen dirs from scripts/check_client_drift.py (cleaned on exit; +# ignored so an interrupted run can't be committed accidentally). +.drift-*/ diff --git a/AGENTS.md b/AGENTS.md index 8d97e78..10ad784 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,10 +70,20 @@ clients/python/ archiver_client SDK v3.x (generated + hand-writte clients/watcher-python/ watcher_client SDK — Archiver adapter for the Watcher service (httpx-based; wraps provision, patch, get, check-now, list-revisions) Regen: bash clients/watcher-python/scripts/regen.sh + watcher-openapi.json: committed OpenAPI snapshot + (contract-of-record). CI `client-drift` job fails + if generated/ != regen-from-snapshot (catches the + #66 stale-client drift). Fix hand-edits: python + scripts/check_client_drift.py --write watcher; on + a real Watcher change re-run regen.sh (refreshes + snapshot + tree). alembic/ Migration root (information schema scoped within the archiver database) tests/ Mirrors src/ structure; tests/integration/ for cross-component flows (HTTP + DB + bus); tests/api/ for single-route HTTP behavior scripts/ dump_openapi.py + + check_client_drift.py (regen vendored clients from + committed OpenAPI snapshots; diff vs generated/; + CI gate, see client-drift job) + check_changelog_on_push.sh (pre-push guard; wired via .pre-commit-config.yaml) deploy/ Systemd unit (archiver.service) @@ -83,8 +93,10 @@ skills-vendor/ Git submodules for external skill repos .claude/skills/ Claude Code skill discovery (symlinks → ../../skills/) .github/workflows/ CI — lint job (ruff check + ruff format --check), test job (Postgres service container, alembic upgrade, - pytest), and changelog job (feat/fix changes must - touch CHANGELOG.md; opt out via `no-changelog` PR + pytest), client-drift job (regen vendored clients + from committed OpenAPI snapshots, fail on diff), + and changelog job (feat/fix changes must touch + CHANGELOG.md; opt out via `no-changelog` PR label). Triggers on push/PR to main. .pre-commit-config.yaml ruff check + ruff format + standard pre-commit-hooks (pre-commit stage), plus a pre-push guard diff --git a/clients/watcher-python/scripts/regen.sh b/clients/watcher-python/scripts/regen.sh index beb745b..f736a75 100755 --- a/clients/watcher-python/scripts/regen.sh +++ b/clients/watcher-python/scripts/regen.sh @@ -1,6 +1,14 @@ #!/usr/bin/env bash -# Regenerate clients/watcher-python/src/watcher_client/generated/ from the -# Watcher service's OpenAPI schema. Idempotent — safe to run repeatedly. +# Regenerate the watcher_client SDK from the Watcher service's live OpenAPI. +# +# Writes BOTH artifacts in lockstep so the CI drift gate +# (scripts/check_client_drift.py) is a no-op afterward: +# 1. clients/watcher-python/watcher-openapi.json — committed contract-of-record +# snapshot (pretty-printed, order-preserving). +# 2. clients/watcher-python/src/watcher_client/generated/ — regenerated FROM +# the snapshot (not the raw live bytes), so the snapshot is authoritative. +# +# Use this when Watcher legitimately changes shape. Idempotent — safe to re-run. # # Requires: Watcher running on http://localhost:8000 # Env: WATCHER_API_KEY (used for authenticated endpoints; openapi.json is public) @@ -9,6 +17,7 @@ set -euo pipefail REPO_ROOT="$(git rev-parse --show-toplevel)" SDK_DIR="${REPO_ROOT}/clients/watcher-python" GEN_DIR="${SDK_DIR}/src/watcher_client/generated" +SNAPSHOT="${SDK_DIR}/watcher-openapi.json" cd "${REPO_ROOT}" TMP_SPEC="$(mktemp -t watcher-openapi-XXXXXX.json)" @@ -16,13 +25,20 @@ trap 'rm -f "${TMP_SPEC}"' EXIT curl -sf http://localhost:8000/openapi.json -o "${TMP_SPEC}" +# Canonicalize into the committed snapshot: pretty-print, order-preserving. +# NOT sort_keys — openapi-python-client emits model fields in spec property +# order, so sorting would reshape (not just reformat) the generated tree. +uv run --no-project python -c "import json,sys; d=json.load(open(sys.argv[1])); open(sys.argv[2],'w').write(json.dumps(d, indent=2)+'\n')" \ + "${TMP_SPEC}" "${SNAPSHOT}" + cd "${SDK_DIR}" rm -rf "${GEN_DIR}" uv run openapi-python-client generate \ - --path "${TMP_SPEC}" \ + --path "${SNAPSHOT}" \ --meta none \ --output-path "${GEN_DIR}" \ --overwrite uv run ruff format "${GEN_DIR}" || true # cosmetic; don't fail regen on format diffs +echo "Regenerated: ${SNAPSHOT}" echo "Regenerated: ${GEN_DIR}" diff --git a/clients/watcher-python/watcher-openapi.json b/clients/watcher-python/watcher-openapi.json new file mode 100644 index 0000000..3151ed2 --- /dev/null +++ b/clients/watcher-python/watcher-openapi.json @@ -0,0 +1,6598 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "watcher", + "version": "0.1.0" + }, + "paths": { + "/api/v1/watched-items/{watched_item_id}/profiles": { + "post": { + "tags": [ + "temporal-profiles" + ], + "summary": "Create Profile", + "description": "Create the temporal profile for a WatchedItem (one per item).", + "operationId": "create_profile_api_v1_watched_items__watched_item_id__profiles_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "temporal-profiles" + ], + "summary": "List Profiles", + "description": "List the WatchedItem's temporal profile (zero or one).", + "operationId": "list_profiles_api_v1_watched_items__watched_item_id__profiles_get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileResponse" + }, + "title": "Response List Profiles Api V1 Watched Items Watched Item Id Profiles Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/profiles/{profile_id}": { + "patch": { + "tags": [ + "temporal-profiles" + ], + "summary": "Update Profile", + "description": "Partially update a temporal profile.", + "operationId": "update_profile_api_v1_watched_items__watched_item_id__profiles__profile_id__patch", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "profile_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Profile Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "temporal-profiles" + ], + "summary": "Delete Profile", + "description": "Delete a temporal profile.", + "operationId": "delete_profile_api_v1_watched_items__watched_item_id__profiles__profile_id__delete", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "profile_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Profile Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/notifications": { + "post": { + "tags": [ + "watched-item-notifications" + ], + "summary": "Create Item Notification", + "description": "Create a watched-item-scoped notification template.", + "operationId": "create_item_notification_api_v1_watched_items__watched_item_id__notifications_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemNotificationTemplateCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationTemplateResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "watched-item-notifications" + ], + "summary": "List Item Notifications", + "description": "List the watched-item-scoped templates for a WatchedItem.", + "operationId": "list_item_notifications_api_v1_watched_items__watched_item_id__notifications_get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationTemplateResponse" + }, + "title": "Response List Item Notifications Api V1 Watched Items Watched Item Id Notifications Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/notifications/effective": { + "get": { + "tags": [ + "watched-item-notifications" + ], + "summary": "List Effective Notifications", + "description": "The full set of templates in scope for this item \u2014 the single F5 surface.\n\nReturns every template whose visibility matches the item (global, the item's\ndomain, and the item itself), regardless of ``is_active`` so the caller can\nshow the complete picture. The per-event ``events`` filter and ``is_active``\nstill gate actual dispatch (see ``dispatch_event_notifications``).", + "operationId": "list_effective_notifications_api_v1_watched_items__watched_item_id__notifications_effective_get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationTemplateResponse" + }, + "title": "Response List Effective Notifications Api V1 Watched Items Watched Item Id Notifications Effective Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/notifications/{template_id}": { + "patch": { + "tags": [ + "watched-item-notifications" + ], + "summary": "Update Item Notification", + "description": "Update an item-scoped template's mutable fields.", + "operationId": "update_item_notification_api_v1_watched_items__watched_item_id__notifications__template_id__patch", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationTemplateUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationTemplateResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "watched-item-notifications" + ], + "summary": "Delete Item Notification", + "description": "Delete an item-scoped template.", + "operationId": "delete_item_notification_api_v1_watched_items__watched_item_id__notifications__template_id__delete", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/notifications/{template_id}/test": { + "post": { + "tags": [ + "watched-item-notifications" + ], + "summary": "Test Item Notification", + "description": "Send a test notification for an item-scoped template via the notifier service.\n\nReturns {success, reason}, never 5xx.", + "operationId": "test_item_notification_api_v1_watched_items__watched_item_id__notifications__template_id__test_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Test Item Notification Api V1 Watched Items Watched Item Id Notifications Template Id Test Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/notifications/templates": { + "post": { + "tags": [ + "notification-templates" + ], + "summary": "Create Template", + "description": "Create a notification template at the requested visibility scope.\n\nThe scope/ref shape is validated by the schema; here we confirm the\nreferenced Domain/WatchedItem actually exists so a bad ref returns 404\nrather than a 500 from the FK violation.", + "operationId": "create_template_api_v1_notifications_templates_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationTemplateCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationTemplateResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "notification-templates" + ], + "summary": "List Templates", + "description": "List notification templates, optionally filtered by visibility/domain.", + "operationId": "list_templates_api_v1_notifications_templates_get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "visibility", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Visibility" + } + }, + { + "name": "domain_name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Domain Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationTemplateResponse" + }, + "title": "Response List Templates Api V1 Notifications Templates Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/notifications/templates/{template_id}": { + "get": { + "tags": [ + "notification-templates" + ], + "summary": "Get Template", + "description": "Fetch a single notification template by id.", + "operationId": "get_template_api_v1_notifications_templates__template_id__get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationTemplateResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "notification-templates" + ], + "summary": "Update Template", + "description": "Partially update a notification template (visibility/refs are immutable).", + "operationId": "update_template_api_v1_notifications_templates__template_id__patch", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationTemplateUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationTemplateResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "notification-templates" + ], + "summary": "Delete Template", + "description": "Delete a template. Templates are standalone post-#200 \u2014 no ref check needed.", + "operationId": "delete_template_api_v1_notifications_templates__template_id__delete", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/notifications/templates/{template_id}/test": { + "post": { + "tags": [ + "notification-templates" + ], + "summary": "Test Template", + "description": "Send a test notification using this template's configured remote channel.", + "operationId": "test_template_api_v1_notifications_templates__template_id__test_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Test Template Api V1 Notifications Templates Template Id Test Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/audit": { + "get": { + "tags": [ + "audit-log" + ], + "summary": "List Audit Entries", + "description": "List audit log entries with optional filters and pagination.\n\nThe WatchedItem association lives in the JSONB ``payload`` (the dedicated\n``watch_id`` FK column was retired with the Watch table in #191).", + "operationId": "list_audit_entries_api_v1_audit_get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "event_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Event Type" + } + }, + { + "name": "watched_item_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Watched Item Id" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLogResponse" + }, + "title": "Response List Audit Entries Api V1 Audit Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/domains": { + "get": { + "tags": [ + "domains" + ], + "summary": "List Domains", + "description": "List all domain configs.", + "operationId": "list_domains_api_v1_domains_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DomainResponse" + }, + "type": "array", + "title": "Response List Domains Api V1 Domains Get" + } + } + } + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ] + } + }, + "/api/v1/domains/{name}": { + "get": { + "tags": [ + "domains" + ], + "summary": "Get Domain", + "description": "Get a domain config by hostname.", + "operationId": "get_domain_api_v1_domains__name__get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DomainResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "domains" + ], + "summary": "Upsert Domain", + "description": "Create or update a domain config (upsert by hostname).\n\nOn create: min_interval defaults to 1.0, current_interval defaults to min_interval.\nOn update: only provided fields are changed.", + "operationId": "upsert_domain_api_v1_domains__name__patch", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DomainPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DomainResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "domains" + ], + "summary": "Delete Domain", + "description": "Delete a domain config.\n\nReturns 409 if any watched items still reference this domain.", + "operationId": "delete_domain_api_v1_domains__name__delete", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/domains/{name}/archive": { + "post": { + "tags": [ + "domains" + ], + "summary": "Archive Domain", + "description": "Archive a domain \u2014 excludes it from rate-limiter sync.", + "operationId": "archive_domain_api_v1_domains__name__archive_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DomainResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/domains/{name}/restore": { + "post": { + "tags": [ + "domains" + ], + "summary": "Restore Domain", + "description": "Restore an archived domain.", + "operationId": "restore_domain_api_v1_domains__name__restore_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DomainResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/probe": { + "post": { + "tags": [ + "probe" + ], + "summary": "Probe Endpoint", + "description": "Probe a URL: follow redirects, return effective URL and domain.", + "operationId": "probe_endpoint_api_v1_probe_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ] + } + }, + "/api/v1/watched-items": { + "get": { + "tags": [ + "watched-items" + ], + "summary": "List Watched Items", + "description": "List WatchedItems. Archived excluded unless ``include_archived=true``.\nFilter by domain hostname with ``domain=`` or by Archiver InfoItem with\n``archiver_info_item_id=``.", + "operationId": "list_watched_items_api_v1_watched_items_get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "include_archived", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Include Archived" + } + }, + { + "name": "domain", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Domain" + } + }, + { + "name": "archiver_info_item_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Archiver Info Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WatchedItemResponse" + }, + "title": "Response List Watched Items Api V1 Watched Items Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "watched-items" + ], + "summary": "Create Watched Item", + "description": "Create a standalone WatchedItem.\n\nTwo paths depending on which anchor is provided:\n\n**InfoItem-linked** (``archiver_info_item_id`` set): validates the InfoItem via the\nArchiver SDK; name defaults to the InfoItem's name.\nErrors: NotFound \u2192 422, AuthError \u2192 500, ServerError/network \u2192 503.\n\n**URL-only** (``url`` set, no ``archiver_info_item_id``): probes the URL for\n``effective_url`` + ``domain_name``; name defaults to the probed domain.\n``archiver_info_item_id`` is null on the resulting record.\nError: unreachable URL \u2192 422.\n\nAt least one of ``archiver_info_item_id`` or ``url`` is required (schema-enforced).", + "operationId": "create_watched_item_api_v1_watched_items_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}": { + "get": { + "tags": [ + "watched-items" + ], + "summary": "Get Watched Item", + "description": "Fetch a single WatchedItem by ID.", + "operationId": "get_watched_item_api_v1_watched_items__watched_item_id__get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "watched-items" + ], + "summary": "Patch Watched Item", + "description": "Update mutable WatchedItem fields. All fields optional.\n\n``is_active`` (pause/resume) cannot be changed on an archived item \u2014 the\narchive/restore lifecycle owns activation while archived. Such a PATCH\nreturns 409; use ``POST /{id}/restore`` to reactivate.\n\nAn ``is_active`` transition emits a dedicated ``WATCHED_ITEM_PAUSED`` /\n``WATCHED_ITEM_RESUMED`` audit event (#189) and is excluded from the\ngeneric ``WATCHED_ITEM_UPDATED`` event, which carries only the other\nchanged fields. A no-op (same value) emits nothing.", + "operationId": "patch_watched_item_api_v1_watched_items__watched_item_id__patch", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "watched-items" + ], + "summary": "Delete Watched Item", + "description": "Permanently delete an archived WatchedItem (#210).\n\nPre-flight: 404 if not found / malformed id; 409 if the item is not archived\n(archive first \u2014 archived already implies ``is_active=False``). On success the\nDB cascades the item's children (``temporal_profiles``,\n``notification_templates``, ``change_revisions``, ``pending_archiver_sync``)\nvia their ``ON DELETE CASCADE`` FKs. An audit row is written before the delete\nand survives it (the WatchedItem id lives in the JSONB payload, not an FK).\nArchiver-side content (InfoItem / SourceRevisions) is left untouched.", + "operationId": "delete_watched_item_api_v1_watched_items__watched_item_id__delete", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/archive": { + "post": { + "tags": [ + "watched-items" + ], + "summary": "Archive Watched Item", + "description": "Archive a WatchedItem (the single monitored entity, #191).\n\nSets ``archived_at`` and flips ``is_active`` to False; the fetch cycle stops\nwithin one ``schedule_tick`` interval because the tick filters on\n``WatchedItem.archived_at IS NULL``.", + "operationId": "archive_watched_item_api_v1_watched_items__watched_item_id__archive_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/restore": { + "post": { + "tags": [ + "watched-items" + ], + "summary": "Restore Watched Item", + "description": "Restore the WatchedItem \u2014 clears ``archived_at`` and re-activates.", + "operationId": "restore_watched_item_api_v1_watched_items__watched_item_id__restore_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/mark-reviewed": { + "post": { + "tags": [ + "watched-items" + ], + "summary": "Mark Reviewed", + "description": "Stamp ``last_reviewed_at = now()``.", + "operationId": "mark_reviewed_api_v1_watched_items__watched_item_id__mark_reviewed_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/check-now": { + "post": { + "tags": [ + "watched-items" + ], + "summary": "Check Now", + "description": "Enqueue an immediate ``check_watched_item`` task for a WatchedItem.\n\nPre-flight guards:\n- 409 if the WatchedItem is archived.\n- 409 if the WatchedItem is paused (``is_active=False``) \u2014 the task would\n short-circuit, so reject up front rather than enqueue a silent no-op.\n- 422 if ``effective_url`` is empty (nothing to fetch).", + "operationId": "check_now_api_v1_watched_items__watched_item_id__check_now_post", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WatchedItemResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/watched-items/{watched_item_id}/revisions": { + "get": { + "tags": [ + "watched-items" + ], + "summary": "List Revisions", + "description": "List ChangeRevisions for a WatchedItem, newest first.", + "operationId": "list_revisions_api_v1_watched_items__watched_item_id__revisions_get", + "security": [ + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChangeRevisionResponse" + }, + "title": "Response List Revisions Api V1 Watched Items Watched Item Id Revisions Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/health": { + "get": { + "tags": [ + "health" + ], + "summary": "Health", + "description": "Liveness probe \u2014 confirms the app process is running. No DB call.", + "operationId": "health_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Health Health Get" + } + } + } + } + } + } + }, + "/ready": { + "get": { + "tags": [ + "health" + ], + "summary": "Ready", + "description": "Readiness probe \u2014 checks DB connectivity and queue accessibility.\n\nReturns 200 when all dependencies are reachable, 503 otherwise.\nThe queue check is a best-effort stub; procrastinate does not expose a\nlightweight ping, so queue is always reported as True unless further\nintrospection is added.", + "operationId": "ready_ready_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Dashboard Home", + "description": "Dashboard home page with stats and system health.\n\nPhase 5 (#156): Recent Changes section removed \u2014 Change table dropped.", + "operationId": "dashboard_home__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/watched-items/new": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Create Form", + "description": "Standalone WatchedItem create form.", + "operationId": "watched_item_create_form_watched_items_new_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + }, + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Create Submit", + "description": "Create a standalone WatchedItem from the dashboard form.\n\nAccepts a URL directly; probes it for effective_url + domain_name. The\ncontent media type is auto-detected from the first fetch (#168), not\ncollected here. Unchecking ``is_active`` provisions the item paused (#188).\nAudit row uses ``source=\"dashboard\"``.", + "operationId": "watched_item_create_submit_watched_items_new_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_watched_item_create_submit_watched_items_new_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Items Page", + "description": "List page for WatchedItems.", + "operationId": "watched_items_page_watched_items_get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "name": "include_archived", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Include Archived" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 25, + "title": "Page Size" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/partials/watched-items-table": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Partial Watched Items Table", + "description": "HTMX partial: watched-items table with search, filter, and pagination.", + "operationId": "partial_watched_items_table_partials_watched_items_table_get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "name": "include_archived", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Include Archived" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 25, + "title": "Page Size" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Detail Page", + "description": "Detail page for a WatchedItem.", + "operationId": "watched_item_detail_page_watched_items__watched_item_id__get", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/archive": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Archive", + "description": "Dashboard archive \u2014 cascades to child Watches (delegates to shared logic).", + "operationId": "watched_item_archive_watched_items__watched_item_id__archive_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/restore": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Restore", + "description": "Dashboard restore \u2014 parent only.", + "operationId": "watched_item_restore_watched_items__watched_item_id__restore_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/delete": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Delete", + "description": "Permanently delete an archived WatchedItem (delegates to the API route, #210).\n\nThe API enforces the guards (404 not found, 409 not archived). On success the\nitem is gone, so we redirect to the list rather than the now-missing detail\npage. A 409 (un-archived) surfaces as an OOB error flash for HTMX, or a\nredirect back to the still-present detail page for non-HTMX clients.", + "operationId": "watched_item_delete_watched_items__watched_item_id__delete_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/mark-reviewed": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Mark Reviewed", + "description": "Stamp last_reviewed_at = now() on a WatchedItem.\n\nDashboard UI for this is pending (#185 Phase A removed the sub_aspect banner\nthat contained the only form). Callable via direct POST or the API route.", + "operationId": "watched_item_mark_reviewed_watched_items__watched_item_id__mark_reviewed_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/toggle-active": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Toggle Active", + "description": "Pause/resume a WatchedItem via the dashboard toggle (#188/#189).\n\nMirrors the API PATCH ``is_active`` semantics: an archived item rejects the\ntoggle (restore owns activation), and resume is blocked while the domain is\nsuspended (kill-switch parity with the Watch toggle). Emits the dedicated\n``WATCHED_ITEM_PAUSED`` / ``WATCHED_ITEM_RESUMED`` audit events. Guard\nrejections re-render the toggle in its true state with an OOB flash (HTMX)\nor redirect back to detail (non-HTMX).", + "operationId": "watched_item_toggle_active_watched_items__watched_item_id__toggle_active_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_watched_item_toggle_active_watched_items__watched_item_id__toggle_active_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/check-now": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Check Now", + "description": "Enqueue an immediate check for a WatchedItem (delegates to the API route).\n\nThe API enforces the pre-flight guards (409 archived/paused, 422 empty\neffective_url). For HTMX, success and guard failures surface as an OOB\nflash; non-HTMX clients get a redirect back to the detail page.", + "operationId": "watched_item_check_now_watched_items__watched_item_id__check_now_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/effective-url/field": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Url Field Partial", + "description": "Serve the WatchedItem URL field partial in view or edit mode.\n\nPowers the inline Edit affordance on the detail page's URL row; the edit\nform posts to the sibling ``/effective-url`` route which re-probes.", + "operationId": "watched_item_url_field_partial_watched_items__watched_item_id__effective_url_field_get", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "mode", + "in": "query", + "required": false, + "schema": { + "enum": [ + "view", + "edit" + ], + "type": "string", + "default": "view", + "title": "Mode" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/effective-url": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Update Url", + "description": "Re-probe a new URL and update the WatchedItem's effective_url + domain_name.\n\nMirrors the create-time probe path: the submitted URL is probed for its\ncanonical effective_url and domain, the Domain row is created if new, and\n``source_specs`` are left untouched. Rejects archived items. ``domain_suspended``\nis re-evaluated against the target Domain so a re-probe can't silently re-arm\nfetching against a suspended domain \u2014 and if the target is suspended the\noperator gets a warning flash instead of the success reload. Probe failures\nsurface as a flash.", + "operationId": "watched_item_update_url_watched_items__watched_item_id__effective_url_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_watched_item_update_url_watched_items__watched_item_id__effective_url_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/field/{field_name}": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Field Partial", + "description": "Serve a single WatchedItem field partial in view or edit mode.", + "operationId": "watched_item_field_partial_watched_items__watched_item_id__field__field_name__get", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "field_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Field Name" + } + }, + { + "name": "mode", + "in": "query", + "required": false, + "schema": { + "enum": [ + "view", + "edit" + ], + "type": "string", + "default": "view", + "title": "Mode" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Field Update", + "description": "Update a single WatchedItem field (HTMX inline edit).", + "operationId": "watched_item_field_update_watched_items__watched_item_id__field__field_name__post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "field_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Field Name" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_watched_item_field_update_watched_items__watched_item_id__field__field_name__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/tags": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Tags Partial", + "operationId": "watched_item_tags_partial_watched_items__watched_item_id__tags_get", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Tag Add", + "operationId": "watched_item_tag_add_watched_items__watched_item_id__tags_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_watched_item_tag_add_watched_items__watched_item_id__tags_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/tags/{tag}": { + "delete": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Tag Remove", + "operationId": "watched_item_tag_remove_watched_items__watched_item_id__tags__tag__delete", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "tag", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tag" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/partials/watched-item-templates/{watched_item_id}": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Templates Partial", + "operationId": "watched_item_templates_partial_partials_watched_item_templates__watched_item_id__get", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/templates/new": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Template New Form", + "operationId": "watched_item_template_new_form_watched_items__watched_item_id__templates_new_get", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/templates": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Template Create", + "operationId": "watched_item_template_create_watched_items__watched_item_id__templates_post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_watched_item_template_create_watched_items__watched_item_id__templates_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/templates/{tpl_id}/edit": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Template Edit Form", + "operationId": "watched_item_template_edit_form_watched_items__watched_item_id__templates__tpl_id__edit_get", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "tpl_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tpl Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/watched-items/{watched_item_id}/templates/{tpl_id}": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Template Update", + "operationId": "watched_item_template_update_watched_items__watched_item_id__templates__tpl_id__post", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "tpl_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tpl Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_watched_item_template_update_watched_items__watched_item_id__templates__tpl_id__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "dashboard" + ], + "summary": "Watched Item Template Delete", + "operationId": "watched_item_template_delete_watched_items__watched_item_id__templates__tpl_id__delete", + "parameters": [ + { + "name": "watched_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Watched Item Id" + } + }, + { + "name": "tpl_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tpl Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Domains Page", + "description": "Domains list page with search, filter, and pagination.", + "operationId": "domains_page_domains_get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "active", + "title": "Status" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 25, + "title": "Page Size" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Create Submit", + "description": "Create domain by probing a URL to extract the effective domain.", + "operationId": "domain_create_submit_domains_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_domain_create_submit_domains_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/partials/domains-table": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Partial Domains Table", + "description": "HTMX partial: domains table with search, filter, and pagination.", + "operationId": "partial_domains_table_partials_domains_table_get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "active", + "title": "Status" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 25, + "title": "Page Size" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/new": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Domain Create Form", + "description": "Domain creation form.", + "operationId": "domain_create_form_domains_new_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/domains/{name}/archive": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Archive", + "description": "Archive a domain from the dashboard.", + "operationId": "domain_archive_domains__name__archive_post", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{name}/restore": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Restore", + "description": "Restore an archived domain from the dashboard.", + "operationId": "domain_restore_domains__name__restore_post", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{name}/toggle-active": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Toggle Active", + "description": "Toggle domain active status.\n\nDeactivating suspends every WatchedItem on the domain (``domain_suspended``);\nreactivating clears the flag. ``domain_suspended`` gates scheduling and the\npause/resume toggle directly \u2014 the WatchedItem is the single monitored entity.", + "operationId": "domain_toggle_active_domains__name__toggle_active_post", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + }, + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "name", + "title": "Sort" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "asc", + "title": "Order" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_domain_toggle_active_domains__name__toggle_active_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{name}/delete": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Delete", + "description": "Hard-delete an archived domain with no watches.\n\nReturns 200 + HX-Redirect on success so HTMX navigates the full page rather\nthan swapping the redirect response into the #danger-zone-error element.\nError cases return HTML fragments suitable for innerHTML swap into that target.", + "operationId": "domain_delete_domains__name__delete_post", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{name}/cadence-field": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Domain Cadence Field Partial", + "description": "Serve the domain Default Interval field partial in view or edit mode (#208).", + "operationId": "domain_cadence_field_partial_domains__name__cadence_field_get", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + }, + { + "name": "mode", + "in": "query", + "required": false, + "schema": { + "enum": [ + "view", + "edit" + ], + "type": "string", + "default": "view", + "title": "Mode" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{name}/field/{field_name}": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Domain Field Partial", + "description": "Serve a single domain field partial in view or edit mode.", + "operationId": "domain_field_partial_domains__name__field__field_name__get", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + }, + { + "name": "field_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Field Name" + } + }, + { + "name": "mode", + "in": "query", + "required": false, + "schema": { + "enum": [ + "view", + "edit" + ], + "type": "string", + "default": "view", + "title": "Mode" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{name}": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Inline Update", + "description": "Update a single domain field (inline edit from detail view).", + "operationId": "domain_inline_update_domains__name__post", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_domain_inline_update_domains__name__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "dashboard" + ], + "summary": "Domain Detail Page", + "description": "Domain detail page with config, watches, and danger zone.", + "operationId": "domain_detail_page_domains__name__get", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + }, + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "name", + "title": "Sort" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "asc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{name}/default-schedule-config": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Default Schedule Config Update", + "description": "Set or clear a domain's default check cadence and back-fill its items (#205).\n\nA blank ``interval`` clears the cadence (items fall back to the system\ndefault). A non-blank value is stored as ``{\"interval\": }`` after\nvalidation; a malformed interval re-renders the detail page with an error\nflash (status 400). Re-denormalizes onto every WatchedItem on the domain.", + "operationId": "domain_default_schedule_config_update_domains__name__default_schedule_config_post", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_domain_default_schedule_config_update_domains__name__default_schedule_config_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{domain_name}/notifications/new": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Domain Notification New Page", + "description": "Full page: create a new notification template for a domain.", + "operationId": "domain_notification_new_page_domains__domain_name__notifications_new_get", + "parameters": [ + { + "name": "domain_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Domain Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Notification Create", + "description": "Create a NotificationTemplate and link to domain. Redirects on success.", + "operationId": "domain_notification_create_domains__domain_name__notifications_new_post", + "parameters": [ + { + "name": "domain_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Domain Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{domain_name}/nc-defaults": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Domain Nc Defaults Partial", + "description": "HTMX partial: notification defaults assigned to a domain.", + "operationId": "domain_nc_defaults_partial_domains__domain_name__nc_defaults_get", + "parameters": [ + { + "name": "domain_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Domain Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/domains/{domain_name}/nc-defaults/remove/{template_id}": { + "post": { + "tags": [ + "dashboard" + ], + "summary": "Domain Nc Default Remove", + "description": "Delete a domain-scoped notification template (#200: removal = delete the row).", + "operationId": "domain_nc_default_remove_domains__domain_name__nc_defaults_remove__template_id__post", + "parameters": [ + { + "name": "domain_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Domain Name" + } + }, + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/partials/stats-cards": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Partial Stats Cards", + "description": "HTMX partial: stats cards only.", + "operationId": "partial_stats_cards_partials_stats_cards_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/partials/system-health": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Partial System Health", + "description": "HTMX partial: queue health and rate limiter.", + "operationId": "partial_system_health_partials_system_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/partials/domain-watched-items/{name}": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Partial Domain Watched Items", + "description": "HTMX partial: domain WatchedItems table with search, sort, and status filter.", + "operationId": "partial_domain_watched_items_partials_domain_watched_items__name__get", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + }, + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "name", + "title": "Sort" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "asc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/audit": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Audit Log Page", + "description": "Audit log page with filtering.", + "operationId": "audit_log_page_audit_get", + "parameters": [ + { + "name": "event_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Event Type" + } + }, + { + "name": "watched_item_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/partials/audit-table": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Partial Audit Table", + "description": "HTMX partial: filtered audit log table.", + "operationId": "partial_audit_table_partials_audit_table_get", + "parameters": [ + { + "name": "event_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Event Type" + } + }, + { + "name": "watched_item_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Watched Item Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/partials/notification-templates-list": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Partial Notification Templates List", + "description": "HTMX partial: notification template table rows (tbody content).", + "operationId": "partial_notification_templates_list_partials_notification_templates_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/notifications/overrides/add-picker": { + "get": { + "tags": [ + "dashboard" + ], + "summary": "Notifications Override Add Picker", + "description": "Return the override picker (a