From 50b706545aba157354443ad7d0b5d553c2c9895d Mon Sep 17 00:00:00 2001 From: gregoryfoster Date: Tue, 23 Jun 2026 22:55:34 +0000 Subject: [PATCH 1/4] =?UTF-8?q?#67=20docs:=20plan=20for=20generated=20API-?= =?UTF-8?q?client=20drift=20detection=20(Layers=20A=E2=80=93D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...026-06-23-detect-generated-client-drift.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs/plans/2026-06-23-detect-generated-client-drift.md diff --git a/docs/plans/2026-06-23-detect-generated-client-drift.md b/docs/plans/2026-06-23-detect-generated-client-drift.md new file mode 100644 index 0000000..33f9973 --- /dev/null +++ b/docs/plans/2026-06-23-detect-generated-client-drift.md @@ -0,0 +1,125 @@ +--- +title: Detect generated API-client drift in CI (watcher_client / archiver-client) +date: 2026-06-23 +status: draft +--- + +# Detect generated API-client drift in CI + +GH issue: CannObserv/archiver#67. Incident this prevents: #66. + +## Problem + +Nothing detects when a vendored generated API client drifts from its upstream +OpenAPI. `watcher_client` (`clients/watcher-python/`) is regenerated by hand from +Watcher's OpenAPI, so when Watcher changes a response shape the archiver runs a +stale client until it fails in prod — exactly #66 (`default_content_type` +dropped → `KeyError` on every parse → all dashboard Watcher actions broken, +surfaced only when an operator clicked "Begin Watching"). The #66 regression +tests pin the *committed* model against hand-written payloads; they pass as long +as committed code and fixtures agree, and never compare against Watcher's *live* +schema. The inverse direction is symmetric: `clients/python/` (archiver-client) +is generated from this service and consumed by Watcher/Replicator, so a route or +schema edit here without a regen ships a stale SDK to those consumers. + +## Approach + +Two drift directions need different mechanisms — the issue's preferred +"CI regenerate + diff" gate is hermetic for one direction and impossible for the +other: + +- **archiver-client** is generated from *this* app. `scripts/dump_openapi.py` + runs in-process (no server, no DB), so CI can regenerate and diff + deterministically and always-current. This catches the inverse drift fully. +- **watcher_client** is generated from the *sibling* Watcher service. Archiver + CI cannot reach live Watcher on a blocking check without coupling to its + uptime. A committed `watcher-openapi.json` snapshot makes the generated tree a + pure function of a reviewed spec — but a snapshot-vs-tree diff only proves + *consistency*, not *currency*: both are committed together and bumped in the + same PR, so they always agree. The snapshot itself going stale relative to + live Watcher is the #66 failure mode and no in-repo diff can see it. Catching + real upstream drift requires comparing against live Watcher, which can only be + a *non-blocking* scheduled job. + +So: ship the two hermetic, PR-blocking gates now (this plan, Layers A+B), and +land the scheduled live-compare detector (Layer C) and runtime canary (Layer D) +as follow-ups. Each generated-tree gate must run inside that SDK's own uv venv +so the locked generator version is used (archiver-client pins +`openapi-python-client` 0.28.3, watcher_client pins 0.29.0 — a shared/floating +generator yields spurious diffs), and mirror `regen.sh` exactly including the +post-generation `ruff format`. + +## Tradeoffs / alternatives + +- **Snapshot-only watcher_client gate (no scheduled detector)** — rejected as + the *sole* watcher mechanism because it cannot detect #66-class upstream + drift; it only catches hand-edits to `generated/`. Kept as Layer B for its + real value (contract-of-record, forces upstream bumps through PR review), but + explicitly paired with Layer C as the follow-up that actually prevents #66. +- **Fetch live Watcher OpenAPI on every PR (blocking)** — rejected: GH runners + are external, so reaching Watcher is flaky/uncontrolled and would redden a + required check on Watcher downtime. Live-compare belongs in a non-blocking + scheduled job (Layer C) where a failed fetch skips the run. +- **Periodic regen bot that opens a PR (issue approach 2) instead of a gate** — + rejected as a *replacement* for the gates: it's softer (drift can sit + un-merged) and doesn't block the inverse archiver-client drift at all. Folded + into Layer C as the remediation arm (detect → regen → open PR). +- **No committed snapshot; archiver-client gate only** — rejected: leaves the + #66 direction (watcher_client) with zero CI signal, which is the issue's + primary ask. + +## Steps + +Layers A + B are the scope of this plan (the two hermetic PR-blocking gates). +Layers C and D are scoped here but deferred to follow-up issues/PRs. + +1. **Drift-check script (shared).** Add a script per SDK (or one parametrized + script) that, inside the SDK's own venv, regenerates the `generated/` tree + into a temp dir from the relevant spec, runs the same `ruff format`, and + diffs against the committed tree — non-zero exit + readable report on drift. + Mirror `regen.sh` mechanics so a green gate means "running regen.sh is a + no-op". +2. **Layer A — archiver-client gate.** Wire the script for `clients/python` to + regenerate from `scripts/dump_openapi.py` and diff. Verify it is green on + current HEAD (fails day-one if the committed tree has any post-generation + hand-edit). +3. **Layer B — watcher_client snapshot + gate.** Commit `watcher-openapi.json` + (the contract-of-record) and have the script regenerate `watcher_client` + *from the snapshot* and diff. Verify green on current HEAD. Document the + snapshot's provenance and how to bump it. +4. **CI wiring.** Add a `client-drift` job to `.github/workflows/ci.yml` + (push + PR to main) running both gates. Confirm the job is green before + making it a required check. +5. **Tests + docs.** TDD the drift-check script's diff/report logic where it has + non-trivial behavior. Update `CHANGELOG.md` only if a contract-visible path + changes (the SDKs/snapshot are tooling; likely a tooling/no-changelog change — + confirm against the `clients/python/` trigger path). Note the new gate in + `CLAUDE.md` CI section if warranted. +6. **Layer C (follow-up).** Scheduled GH Action: fetch live Watcher + `/openapi.json` (public), diff vs committed `watcher-openapi.json`; on drift + run `regen.sh` and open a PR. Non-blocking. Prefer a Watcher-side push + (`repository_dispatch` / Watcher CI opens the bump PR here) over archiver + polling — same owner, cleaner signal. File as its own issue. +7. **Layer D (follow-up).** Runtime canary using the existing + `WatcherResponseError` typed signal: startup/health probe parses a known + Watcher response and alerts on drift. Backstop, not gate. File as its own + issue. + +## Open questions / risks + +- **Generator/`ruff` version drift in CI.** The two SDKs pin different + `openapi-python-client` versions under a loose `>=0.21,<1` constraint; each + gate must use the SDK's own lockfile, and `ruff` format version must match + regen's, or the gate flakes. Mitigation: run each gate via + `uv run --project ` against the committed lockfile. +- **Day-one red gate.** If any committed `generated/` tree was hand-tweaked + post-generation, the gate fails immediately. Must regenerate-and-commit (or + fix the tweak) before the check is required. Verify in steps 2–3. +- **Snapshot provenance for Layer B.** Where does the first + `watcher-openapi.json` come from in CI — committed from a known-good live pull? + It is the contract-of-record, so it must be captured from a Watcher version the + current `watcher_client` actually matches (else step 3 is red on day one). +- **Layer C network/secrets.** Live-compare needs a *public* Watcher URL from GH + runners (not `localhost`/proxy — hairpin NAT note in CLAUDE.md is VM-internal + and doesn't apply to runners). `openapi.json` is public per `regen.sh`, so + likely no API key. Confirm when Layer C is built. From a0402f4d3d2db7877d01f5e7475c2c76b6ed3663 Mon Sep 17 00:00:00 2001 From: gregoryfoster Date: Wed, 24 Jun 2026 00:04:36 +0000 Subject: [PATCH 2/4] #67 ci: detect watcher_client drift vs committed OpenAPI snapshot (Layer B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a hermetic, PR-blocking gate that turns a stale generated client (the #66 failure mode) into a red build: - clients/watcher-python/watcher-openapi.json — committed contract-of-record snapshot of Watcher's OpenAPI (order-preserving pretty-print; generation- identical to the live spec). - scripts/check_client_drift.py — regenerates the client from the snapshot into a temp dir *inside the SDK tree* (so ruff resolves the same config as regen.sh) and diffs vs the committed generated/ tree. --write regenerates in place as drift remediation. Extensible client registry. - tests/scripts/test_check_client_drift.py — TDD coverage of diff_trees. - .github/workflows/ci.yml — client-drift job (push/PR to main). Gate is green against the pristine HEAD tree (no generated-tree change). This is consistency-only (snapshot vs tree); detecting drift of the snapshot itself vs live Watcher needs a scheduled live-compare (Layer C, follow-up). The archiver-client direction (Layer A) is deferred — its committed SDK is stale (missing /api/v1/domains models) and regen.sh doesn't prune /dashboard routes; filed separately. See docs/plans/2026-06-23-detect-generated-client-drift.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 22 + .gitignore | 4 + AGENTS.md | 14 +- clients/watcher-python/watcher-openapi.json | 6598 +++++++++++++++++ ...026-06-23-detect-generated-client-drift.md | 31 +- scripts/check_client_drift.py | 205 + tests/scripts/test_check_client_drift.py | 105 + 7 files changed, 6976 insertions(+), 3 deletions(-) create mode 100644 clients/watcher-python/watcher-openapi.json create mode 100644 scripts/check_client_drift.py create mode 100644 tests/scripts/test_check_client_drift.py 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..476f7c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,10 +70,18 @@ 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: python + scripts/check_client_drift.py --write watcher. 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 +91,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/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