feat(webhooks): persist delivery log + replay UI (Phase 3)#170
Merged
pratyush618 merged 3 commits intoMay 17, 2026
Merged
Conversation
Every webhook attempt now lands in a per-subscription JSON list under
``webhooks:deliveries:{sub_id}`` in the dashboard settings store, capped
at 200 entries per webhook with FIFO eviction. Records carry the final
HTTP status, response body (truncated to 2 KiB), latency, attempt count,
and any transport-level error so operators can debug failures without
leaving the dashboard.
The dispatcher gains GET /api/webhooks/{id}/deliveries (with
status/event/limit/offset filters), GET /api/webhooks/{id}/deliveries/
{delivery_id}, and POST /api/webhooks/{id}/deliveries/{delivery_id}/
replay — replay re-fires the original payload synchronously and records
the outcome as a fresh delivery.
New ``/webhooks/{id}/deliveries`` route surfaces the persisted delivery
log per webhook. Rows show timestamp, event, status badge, response
code, latency, and attempts; clicking a row opens a dialog with the
full payload and (truncated) response body. Each row carries a Replay
button — and so does the row-actions menu on the main webhooks list
("View deliveries"). Status filter lets operators narrow to delivered/
failed/dead quickly.
This was referenced May 17, 2026
* feat(overrides): persist per-task and per-queue runtime overrides Operators can now tune retry policy, concurrency, rate limit, timeout, priority, and pause state per task — and rate limit, concurrency, pause state per queue — without code changes. Overrides live as JSON entries under ``overrides:task:*`` and ``overrides:queue:*`` keys in the dashboard settings store, and merge into PyTaskConfig / queue_configs at worker startup. Pause state additionally flips the existing paused_queues mechanism so it takes effect on a running worker. Queue gains ``set_task_override`` / ``clear_task_override`` / ``set_queue_override`` / ``clear_queue_override`` plus discovery APIs ``registered_tasks()`` / ``registered_queues()`` that surface decorator defaults, overrides, and effective values for the dashboard. * feat(dashboard): add Tasks page with override editor New /tasks route lists every registered task with its decorator defaults, any active runtime override, and the effective values. Edit-button opens a side sheet with a form for rate_limit, max_concurrent, max_retries, timeout, priority, and a paused toggle. Empty inputs mean "inherit decorator default"; the clear-override button removes the row entirely. Effective values that differ from decorator defaults are highlighted in accent. * feat(middleware): per-task middleware toggles (Phase 5) (#172) * feat(middleware): toggle middleware per task from dashboard TaskMiddleware now carries a stable ``name`` (defaulting to the fully- qualified class path so it survives restarts; overridable on a subclass). Per-task disable lists live under ``middleware:disabled:<task>`` in the dashboard settings store, consulted by ``_get_middleware_chain`` at every invocation — turning a middleware off for one task takes effect on the next job, no worker restart needed. Queue gains ``list_middleware()``, ``disable_middleware_for_task``, ``enable_middleware_for_task``, ``clear_middleware_disables``. New endpoints: GET /api/middleware (discovery), GET/DELETE /api/tasks/{task}/middleware, PUT /api/tasks/{task}/middleware/{mw_name}. * feat(dashboard): add Middleware tab to task editor The task override side-sheet now has two tabs: Overrides (existing form) and Middleware. The Middleware tab lists every middleware that fires for the selected task with an enabled/disabled toggle each. Toggling a middleware off takes effect on the next job — no worker restart required.
pratyush618
added a commit
that referenced
this pull request
May 17, 2026
#169) * feat(webhooks): persist subscriptions and add CRUD endpoints Webhook subscriptions are stored as JSON in the dashboard_settings table, so they survive restarts and propagate across every worker pointed at the same backend. WebhookManager reloads its in-memory snapshot on every CRUD write. Each subscription supports an optional per-task filter alongside event-type filtering, configurable retry policy, and HMAC signing with a rotatable secret. An SSRF guard rejects loopback / RFC1918 / link-local destinations unless TASKITO_WEBHOOKS_ALLOW_PRIVATE is set. The dashboard exposes list / get / create / update / delete / rotate-secret / send-test endpoints plus a public GET /api/event-types listing. * feat(dashboard): add Webhooks page with full CRUD UI New /webhooks route lists configured subscriptions and surfaces a create dialog with event-type multi-select, optional per-task filter input, and auto-generated signing secret. Each row gets a dropdown menu for send-test, enable/disable toggle, secret rotation (with confirmation gate), and delete. Newly minted secrets are shown once through a reveal-and-copy card that hides the raw value on close. * feat(webhooks): persist delivery log + replay UI (Phase 3) (#170) * feat(webhooks): persist delivery log with replay support Every webhook attempt now lands in a per-subscription JSON list under ``webhooks:deliveries:{sub_id}`` in the dashboard settings store, capped at 200 entries per webhook with FIFO eviction. Records carry the final HTTP status, response body (truncated to 2 KiB), latency, attempt count, and any transport-level error so operators can debug failures without leaving the dashboard. The dispatcher gains GET /api/webhooks/{id}/deliveries (with status/event/limit/offset filters), GET /api/webhooks/{id}/deliveries/ {delivery_id}, and POST /api/webhooks/{id}/deliveries/{delivery_id}/ replay — replay re-fires the original payload synchronously and records the outcome as a fresh delivery. * feat(dashboard): add deliveries route with filter + replay New ``/webhooks/{id}/deliveries`` route surfaces the persisted delivery log per webhook. Rows show timestamp, event, status badge, response code, latency, and attempts; clicking a row opens a dialog with the full payload and (truncated) response body. Each row carries a Replay button — and so does the row-actions menu on the main webhooks list ("View deliveries"). Status filter lets operators narrow to delivered/ failed/dead quickly. * feat(overrides): runtime task & queue overrides (Phase 4) (#171) * feat(overrides): persist per-task and per-queue runtime overrides Operators can now tune retry policy, concurrency, rate limit, timeout, priority, and pause state per task — and rate limit, concurrency, pause state per queue — without code changes. Overrides live as JSON entries under ``overrides:task:*`` and ``overrides:queue:*`` keys in the dashboard settings store, and merge into PyTaskConfig / queue_configs at worker startup. Pause state additionally flips the existing paused_queues mechanism so it takes effect on a running worker. Queue gains ``set_task_override`` / ``clear_task_override`` / ``set_queue_override`` / ``clear_queue_override`` plus discovery APIs ``registered_tasks()`` / ``registered_queues()`` that surface decorator defaults, overrides, and effective values for the dashboard. * feat(dashboard): add Tasks page with override editor New /tasks route lists every registered task with its decorator defaults, any active runtime override, and the effective values. Edit-button opens a side sheet with a form for rate_limit, max_concurrent, max_retries, timeout, priority, and a paused toggle. Empty inputs mean "inherit decorator default"; the clear-override button removes the row entirely. Effective values that differ from decorator defaults are highlighted in accent. * feat(middleware): per-task middleware toggles (Phase 5) (#172) * feat(middleware): toggle middleware per task from dashboard TaskMiddleware now carries a stable ``name`` (defaulting to the fully- qualified class path so it survives restarts; overridable on a subclass). Per-task disable lists live under ``middleware:disabled:<task>`` in the dashboard settings store, consulted by ``_get_middleware_chain`` at every invocation — turning a middleware off for one task takes effect on the next job, no worker restart needed. Queue gains ``list_middleware()``, ``disable_middleware_for_task``, ``enable_middleware_for_task``, ``clear_middleware_disables``. New endpoints: GET /api/middleware (discovery), GET/DELETE /api/tasks/{task}/middleware, PUT /api/tasks/{task}/middleware/{mw_name}. * feat(dashboard): add Middleware tab to task editor The task override side-sheet now has two tabs: Overrides (existing form) and Middleware. The Middleware tab lists every middleware that fires for the selected task with an enabled/disabled toggle each. Toggling a middleware off takes effect on the next job — no worker restart required.
pratyush618
added a commit
that referenced
this pull request
May 17, 2026
* feat(dashboard): add session-based auth with setup flow Users + sessions live in the existing dashboard_settings KV store, so all three storage backends work without new tables. Passwords use stdlib PBKDF2-HMAC-SHA256 (600k iters, OWASP 2023+ baseline). Session cookie is HttpOnly + SameSite=Strict; state-changing routes require a double-submit CSRF token. Until the first admin is created via /api/auth/setup or the TASKITO_DASHBOARD_ADMIN_USER / TASKITO_DASHBOARD_ADMIN_PASSWORD env vars, every protected route returns 503 setup_required. * feat(dashboard): add login UI, CSRF handling, and auth gate Login page wraps setup + sign-in forms. AuthGate component bounces unauthenticated visitors at the AppShell boundary; the /login route is the only public client-side path. UserMenu in the header surfaces the current user with a sign-out action. api-client now reads the taskito_csrf cookie and forwards it as X-CSRF-Token on POST/PUT/DELETE and uses credentials: same-origin so session cookies travel with every request. * feat(webhooks): persistent subscriptions + CRUD dashboard UI (Phase 2) (#169) * feat(webhooks): persist subscriptions and add CRUD endpoints Webhook subscriptions are stored as JSON in the dashboard_settings table, so they survive restarts and propagate across every worker pointed at the same backend. WebhookManager reloads its in-memory snapshot on every CRUD write. Each subscription supports an optional per-task filter alongside event-type filtering, configurable retry policy, and HMAC signing with a rotatable secret. An SSRF guard rejects loopback / RFC1918 / link-local destinations unless TASKITO_WEBHOOKS_ALLOW_PRIVATE is set. The dashboard exposes list / get / create / update / delete / rotate-secret / send-test endpoints plus a public GET /api/event-types listing. * feat(dashboard): add Webhooks page with full CRUD UI New /webhooks route lists configured subscriptions and surfaces a create dialog with event-type multi-select, optional per-task filter input, and auto-generated signing secret. Each row gets a dropdown menu for send-test, enable/disable toggle, secret rotation (with confirmation gate), and delete. Newly minted secrets are shown once through a reveal-and-copy card that hides the raw value on close. * feat(webhooks): persist delivery log + replay UI (Phase 3) (#170) * feat(webhooks): persist delivery log with replay support Every webhook attempt now lands in a per-subscription JSON list under ``webhooks:deliveries:{sub_id}`` in the dashboard settings store, capped at 200 entries per webhook with FIFO eviction. Records carry the final HTTP status, response body (truncated to 2 KiB), latency, attempt count, and any transport-level error so operators can debug failures without leaving the dashboard. The dispatcher gains GET /api/webhooks/{id}/deliveries (with status/event/limit/offset filters), GET /api/webhooks/{id}/deliveries/ {delivery_id}, and POST /api/webhooks/{id}/deliveries/{delivery_id}/ replay — replay re-fires the original payload synchronously and records the outcome as a fresh delivery. * feat(dashboard): add deliveries route with filter + replay New ``/webhooks/{id}/deliveries`` route surfaces the persisted delivery log per webhook. Rows show timestamp, event, status badge, response code, latency, and attempts; clicking a row opens a dialog with the full payload and (truncated) response body. Each row carries a Replay button — and so does the row-actions menu on the main webhooks list ("View deliveries"). Status filter lets operators narrow to delivered/ failed/dead quickly. * feat(overrides): runtime task & queue overrides (Phase 4) (#171) * feat(overrides): persist per-task and per-queue runtime overrides Operators can now tune retry policy, concurrency, rate limit, timeout, priority, and pause state per task — and rate limit, concurrency, pause state per queue — without code changes. Overrides live as JSON entries under ``overrides:task:*`` and ``overrides:queue:*`` keys in the dashboard settings store, and merge into PyTaskConfig / queue_configs at worker startup. Pause state additionally flips the existing paused_queues mechanism so it takes effect on a running worker. Queue gains ``set_task_override`` / ``clear_task_override`` / ``set_queue_override`` / ``clear_queue_override`` plus discovery APIs ``registered_tasks()`` / ``registered_queues()`` that surface decorator defaults, overrides, and effective values for the dashboard. * feat(dashboard): add Tasks page with override editor New /tasks route lists every registered task with its decorator defaults, any active runtime override, and the effective values. Edit-button opens a side sheet with a form for rate_limit, max_concurrent, max_retries, timeout, priority, and a paused toggle. Empty inputs mean "inherit decorator default"; the clear-override button removes the row entirely. Effective values that differ from decorator defaults are highlighted in accent. * feat(middleware): per-task middleware toggles (Phase 5) (#172) * feat(middleware): toggle middleware per task from dashboard TaskMiddleware now carries a stable ``name`` (defaulting to the fully- qualified class path so it survives restarts; overridable on a subclass). Per-task disable lists live under ``middleware:disabled:<task>`` in the dashboard settings store, consulted by ``_get_middleware_chain`` at every invocation — turning a middleware off for one task takes effect on the next job, no worker restart needed. Queue gains ``list_middleware()``, ``disable_middleware_for_task``, ``enable_middleware_for_task``, ``clear_middleware_disables``. New endpoints: GET /api/middleware (discovery), GET/DELETE /api/tasks/{task}/middleware, PUT /api/tasks/{task}/middleware/{mw_name}. * feat(dashboard): add Middleware tab to task editor The task override side-sheet now has two tabs: Overrides (existing form) and Middleware. The Middleware tab lists every middleware that fires for the selected task with an enabled/disabled toggle each. Toggling a middleware off takes effect on the next job — no worker restart required. * fix(webhooks): only start delivery thread when subscriptions exist WebhookManager.reload() unconditionally spawned a daemon thread on every Queue construction, leaking a thread per test and exhausting the macOS runner's thread limit (Resource temporarily unavailable / can't start new thread) by the time the workflow suite ran. * docs(dashboard): customization guides + screenshots (Phases 1-5) (#173) * fix(dashboard): URL-decode regex captures and unnest deliveries route The HTTP server captured ``[^/]+`` from request paths and used the match verbatim as a settings key / task name. Browser clients run ``encodeURIComponent`` on names containing ``<``, ``>``, ``/`` etc., so the per-task middleware disable list was being written under one key (encoded) and looked up under another (decoded). The chain filter then never matched and the toggle silently no-op'd. Decode every captured group with ``urllib.parse.unquote`` before it reaches a handler. Regression-tested in ``test_put_task_middleware_handles_url_encoded_name``. Also rename ``webhooks.\$id.deliveries.tsx`` to ``webhooks_.\$id.deliveries.tsx`` so the route is a flat sibling of ``/webhooks`` instead of a nested child — without the underscore, TanStack Router required an ``<Outlet />`` on the parent, which the list page doesn't render, so the deliveries page never mounted. * chore(docs): add reproducible screenshot capture pipeline ``scripts/capture_docs_screenshots.py`` seeds a fresh queue with deterministic demo data (admin, sample tasks/queues, three webhook subscriptions with mixed delivery outcomes, a task override, a middleware disable), boots the dashboard on a random port, and walks through every screen with headless Chrome via Playwright. Output lands under ``docs/public/screenshots/dashboard/`` so the MDX side can reference stable paths. ``scripts/optimize_screenshots.py`` follows up with Pillow-based palette quantisation, shaving the bundle by ~71% (2.3 MB → 675 KB) with no visible quality loss on the UI-dominated screenshots. Playwright lives behind a new ``docs`` optional dependency (``uv sync --extra docs``) so contributors who never touch docs don't pay the install cost. * docs(dashboard): customization guides + screenshots for phases 1-5 Two new pages and a rewrite of the existing webhooks guide cover every piece of customisation that landed across PRs #168-#172: - ``observability/dashboard-auth`` — first-run setup, env-var bootstrap, session cookies, CSRF model, headless usage, SSRF guard - ``observability/task-overrides`` — per-task and per-queue runtime knobs, the "next restart vs. takes effect now" timing model, and the middleware on/off matrix - ``extensibility/events-webhooks`` — rewritten to cover both the dashboard CRUD flow and the Python API, including delivery log, replay, secret rotation, and the SSRF guard The existing ``observability/dashboard`` page is refreshed: the outdated "no auth" callout is gone, the page table now lists Webhooks and Tasks, and the walkthrough section is restructured into screen groups so it's easy to skim. The REST API reference (``observability/dashboard-api``) gains sections for Auth, Webhooks, Webhook Deliveries, Tasks & Overrides, Queue Overrides, and Middleware, plus a refreshed lead paragraph (auth + CSRF apply to every route, no more ``Access-Control-Allow-Origin: *``). 12 screenshots under ``docs/public/screenshots/dashboard/`` illustrate the auth flow, every customisation page, and the side-sheet editors — all produced by the new reproducible capture pipeline. * docs: update dashboard screenshots for task edit middleware and task list * chore: remove unused Makefile for documentation generation * feat(dashboard): oauth foundation (config, state store, identity, auth.py) (#174) * chore(deps): add authlib as optional 'oauth' extra * feat(dashboard): add is_safe_redirect for same-origin OAuth next-URLs * feat(dashboard): OAuth config, state store, and provider contract Adds the framework-free foundation for OAuth/OIDC login: - OAuthConfig with env-var parsing (Google, GitHub, multiple named OIDC slots) - OAuthStateStore for short-lived state/nonce/PKCE rows (single-use, 5-min TTL) - ProviderIdentity dataclass + OAuthProvider Protocol for upcoming providers * feat(dashboard): map OAuth identities onto AuthStore users User gains email and display_name fields. verify_password rejects any hash prefixed 'oauth:' so OAuth-only users can never log in via password. get_or_create_oauth_user applies the bootstrap rule: an explicit TASKITO_DASHBOARD_OAUTH_ADMIN_EMAILS list takes precedence; otherwise the first OAuth user on an empty table becomes admin. Verified email is required for any admin path. * feat(dashboard): oauth providers, flow, and HTTP endpoints (#175) * feat(dashboard): oauth provider implementations, flow, and HTTP routes Adds the complete OAuth login pipeline on top of the foundation from PR #174: - GoogleProvider (OIDC + email-domain allowlist) - GitHubProvider (OAuth2 + verified primary email + org membership) - GenericOIDCProvider (discovery-driven, multi-slot for Okta / Microsoft / Auth0) - OAuthFlow orchestrator: state mint, code exchange, allowlist check, AuthStore user/session creation - HTTP endpoints: GET /api/auth/providers, GET /api/auth/oauth/start/{slot}, GET /api/auth/oauth/callback/{slot} — all public, single-use state, S256 PKCE, nonce-verified ID tokens, same cookie shape as password login * test(dashboard): integration tests for OAuth HTTP endpoints Spins up a real ThreadingHTTPServer with a stubbed OAuthFlow to drive the full request -> 302 -> cookies path: provider listing, start redirect, callback success, replayed-state rejection, allowlist denial, and unsafe-next-URL scrubbing. 12 tests. * feat(dashboard): oauth login UI and operator docs (#176) * feat(dashboard): oauth provider buttons on the login screen LoginForm fetches /api/auth/providers and renders one button per enabled provider above the password fields. Each button is a plain anchor to /api/auth/oauth/start/{slot} so the browser handles the 302 natively, no client-side OAuth state to manage. Password form hides entirely when password_enabled is false (OAuth-only mode); next-URL from the route's search params is forwarded so post-login lands on the intended page. * test(dashboard): cover oauthStartUrl encoding edge cases * docs(dashboard): SSO operator setup guide New page walks through registering OAuth clients with Google, GitHub, and generic OIDC providers (Okta, Microsoft, Auth0, Keycloak), env-var reference, allowlist semantics, role-bootstrap rule, OAuth-only mode, and a troubleshooting cookbook. Wires into the observability nav after dashboard-auth, which gains a short forward-reference. * fix(dashboard): mypy 2.x strictness for oauth providers Introduce an HttpClient Protocol so providers accept any duck-typed session (requests.Session in prod, StubSession in tests) without runtime cast(). This is the actual design fix — the test suppressions were patching a missing abstraction. For the joserfc KeySet.import_key_set call, mypy 2.x widened the stub to accept dict-shaped JWKS while mypy 1.x still requires the explicit KeySetSerialization TypedDict. Suppress both directions with the documented arg-type,unused-ignore dual pattern. Encode the test JWT helper return as an explicit str assignment so the no-any-return check passes under both mypy versions. * fix(ci): install oauth extra so dashboard tests can import authlib uv sync --extra dev was leaving authlib/joserfc/requests uninstalled, so the oauth test modules failed collection with ModuleNotFoundError. The oauth deps belong in the oauth extra (taskito core users shouldn't pull authlib + requests for free), so CI sync needs both extras. Also pin requests explicitly in the oauth extra — authlib does not declare it as a hard dep (only its requests_client integration uses it), so leaving it implicit broke clean installs. * docs: dedicated Dashboard section with Mermaid SSO diagram The Dashboard pages outgrew Observability — five pages (overview, auth, SSO, task overrides, REST API) versus three actual observability topics. Move them into their own top-level Guides section and drop the redundant 'Dashboard ' prefix from page titles ('Dashboard Authentication' -> 'Authentication', etc.). Replace the broken ASCII sequence diagram on the SSO page with a Mermaid sequenceDiagram — the ASCII version had misaligned vertical lines once the cookie text on the last step pushed the row wider than the others, and the auto-overflow scroll truncated arrows. The Mermaid version renders correctly at every viewport. All cross-section links updated; old /guides/observability/dashboard* paths now redirect via internal references. * fix(serializers): narrow decryption exception handling EncryptedSerializer.loads caught every Exception and rewrapped it as ValueError, which: 1. Masked programmer errors (e.g. corrupted internal state, MemoryError) that should propagate untouched. 2. Made the two negative tests fail under any environment where cryptography is installed — they asserted InvalidTag, but the wrapper meant ValueError reached them. They were silently green only because the dev extra never installed cryptography. The oauth extra (joserfc) does, and CI surfaced the latent bug. Narrow the catch to cryptography.exceptions.InvalidTag (the only expected failure mode) and pre-cache it on the instance so loads avoids a per-call import. Tests now assert the public contract — ValueError with 'Decryption failed' — plus verify the original InvalidTag survives in __cause__ for debugging. Adds a regression test for the short-input path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3 of the dashboard customization plan. Webhooks now keep a persistent record of every delivery attempt so operators can debug failures, view payloads, and replay events from the dashboard.
Backend
py_src/taskito/dashboard/delivery_store.py—DeliveryStorepersists each attempt underwebhooks:deliveries:{subscription_id}in the existing settings table. FIFO eviction caps each subscription at 200 entries (configurable), so unbounded growth never becomes a footgun. Response bodies are truncated to 2 KiB before storage.py_src/taskito/webhooks.py— every_sendoutcome (delivered / failed / dead) now writes a record with HTTP status, latency, attempt count, response body, and any transport error. The synchronous test-event path (deliver_now) deliberately does NOT write to the log so test pings don't pollute history.py_src/taskito/dashboard/handlers/webhook_deliveries.py+ 3 new routes:GET /api/webhooks/{id}/deliveries— list withstatus/event/limit/offsetfiltersGET /api/webhooks/{id}/deliveries/{delivery_id}— single recordPOST /api/webhooks/{id}/deliveries/{delivery_id}/replay— re-fires the stored payload synchronously and records the outcome as a fresh delivery (the original record is preserved)server.pynow supports route patterns with two captured groups so the deliveries sub-path can be matched cleanly.Frontend
dashboard/src/routes/webhooks.$id.deliveries.tsx— new route, accessible via ""View deliveries"" in the webhooks row-action menu.DeliveryListTable— timestamp, event, status badge, response code, latency, attempt count. Clicking a row opens a detail dialog with the full payload, error, and response body. Inline ""Replay"" button on each row plus a primary replay action in the detail dialog.Delivered/Failed/Dead/All) and manual refresh.api.ts+hooks.tsextended withlistDeliveries/getDelivery/replayDeliveryand matching TanStack Query hooks; the row-actions menu now links to the deliveries route.Test plan
uv run python -m pytest tests/dashboard/test_webhook_deliveries.py— 14 passeduv run python -m pytest tests/dashboard/(full suite incl. earlier phases) — all passuv run ruff check py_src/ tests/cleanuv run mypy py_src/taskito/ tests/ --no-incrementalcleanpnpm --dir dashboard typecheckcleanpnpm --dir dashboard lint(biome) cleanpnpm --dir dashboard test— 102 passedpnpm --dir dashboard buildsucceedsNew tests in
tests/dashboard/test_webhook_deliveries.py:Notes for follow-up (Phase 3b, if needed)
The KV-list storage is appropriate for typical webhook volumes (≤ thousands of deliveries per day). For very high-throughput setups, a dedicated
webhook_deliveriestable with proper indexes is the natural next step — theDeliveryStoreinterface is small and won't change.