# CLAUDE.md — Phase 4: Event Bus and Webhook Ingress

## Scope of This File

This file instructs you (Claude Code) to implement **Phase 4 only** of the Seedbox Command Center.

**Do not implement phases 5 through 15.** Stay in scope.

## Authoritative Specification

`PROJECT.md` §5.5 is authoritative for the event bus and integration layer. Read it before starting. Also read:
- `PROGRESS.md` for current state.
- `DECISIONS.md` for binding decisions.
- The Phase 3 `internal/eventbus` skeleton — Phase 4 promotes it to a first-class subsystem.

## Phase 4 Mission

Promote the system from poll-driven to event-driven. Phase 4 builds the typed internal event bus and the HMAC-validated webhook ingress that the integrated tools (autobrr, qBittorrent, cross-seed) publish to. Cron-driven scrapers stay in place as reconciliation paths, but the primary data flow shifts to webhooks for everything that can be event-sourced.

This phase doesn't add visible UI features. It builds the spine that Phase 5 (filter performance), Phase 6 (rules engine), and several later phases will depend on.

## Deliverables Checklist

Phase 4 is complete when every item below is true:

- [ ] `internal/eventbus` provides a typed pub/sub abstraction over Go channels with named topics. Topics are declared in `internal/eventbus/topics.go` as constants; each topic has a typed payload. New topics in this phase: `torrent.completed`, `torrent.state_changed`, `tracker.status_changed`, `autobrr.grab`, `crossseed.match_found`, `crossseed.match_applied`, plus the `torrents` topic from Phase 3 (which becomes a derived view, fed by torrent.* events).
- [ ] `internal/webhooks` provides an HMAC-validated webhook receiver pattern:
    - One generic `Handler` type that takes a `WebhookHandler` interface (Validate, Process).
    - Per-source handler implementations: `autobrr.go`, `qbit.go`, `crossseed.go`.
    - Validation: HMAC-SHA256 over the raw body, secret from `secrets` keyed `webhook:<endpoint_id>:token`; constant-time comparison.
    - 401 on missing or invalid signature; 400 on malformed body; 200 with `{"received": true}` on success; 202 on accept-but-defer for slow processors.
- [ ] SQLite migration `005_phase4_webhooks.sql` adds the `webhook_endpoints` table per `PROJECT.md` Appendix A.
- [ ] Public webhook routes (mounted at `/webhook/*`, no session required, HMAC required):
    - `POST /webhook/autobrr/:endpoint_id`
    - `POST /webhook/qbit/:endpoint_id`
    - `POST /webhook/crossseed/:endpoint_id`
    - `POST /webhook/generic/:endpoint_id` — operator-defined script-friendly receiver that publishes to a custom topic.
- [ ] Authenticated management routes (under `/api/webhooks`):
    - `GET /api/webhooks` — list configured endpoints.
    - `POST /api/webhooks` — create; generates a fresh secret token, returns it once.
    - `PATCH /api/webhooks/:id` — toggle enabled, update handler_type.
    - `DELETE /api/webhooks/:id`.
- [ ] `GET /sse/events?topic=...` — Server-Sent Events stream for one-way push of topic events to the frontend. The frontend subscribes to topics relevant to the visible page and invalidates `@tanstack/react-query` caches on incoming events.
- [ ] qBittorrent integration extended: the `external-program-on-event` hook is wired by writing a tiny shell template (`scripts/qbit-hook-template.sh`) that operators install in qBittorrent and that posts to `/webhook/qbit/:id`. Document the install step in `DEPLOYMENT.md`.
- [ ] autobrr webhook subscription: the operator configures autobrr to call `/webhook/autobrr/:id` on filter match; the Command Center processes and publishes to `autobrr.grab` on the bus.
- [ ] cross-seed webhook subscription: same shape; publishes to `crossseed.match_found` and `crossseed.match_applied`.
- [ ] Reconciliation: a periodic job (`internal/scrape/reconcile.go`) re-fetches state from each integrated tool every N minutes (default 15) and emits typed events for any state the webhook stream missed. The reconciler shares snapshot writers with Phase 3.
- [ ] Tests: HMAC verification rejects tampered bodies; eventbus delivers to multiple subscribers with backpressure; SSE handler delivers exactly the topics the client subscribed to; reconciler emits events only for state changes (idempotent on no-change inputs).
- [ ] `PROGRESS.md` updated, including a worked example of an autobrr grab traveling through the system: webhook → handler → bus → snapshot writer → SSE → frontend toast.
- [ ] `DECISIONS.md` records: which WebSocket library (carrying over from Phase 3 or chosen here), backpressure policy (drop-oldest vs. block vs. unbounded), and the eventbus's at-most-once delivery semantics.

## Phase 4 Database Scope

Migration `005_phase4_webhooks.sql` creates:
- `webhook_endpoints`

No DuckDB schema changes.

## Repository Layout

```
internal/
  eventbus/
    bus.go                   # promoted to support multiple subscribers, backpressure
    topics.go
    typed.go                 # generic helpers for typed pub/sub
    *_test.go
  webhooks/
    handler.go               # generic HMAC + dispatch
    autobrr.go
    qbit.go
    crossseed.go
    generic.go
    *_test.go
  scrape/
    reconcile.go             # NEW: reconciliation loop
scripts/
  qbit-hook-template.sh      # operator-installed external-program template
```

Files added under existing packages:
- `internal/server/routes_webhooks.go` — authenticated CRUD.
- `internal/server/routes_webhook_ingress.go` — public webhook receivers (no auth).
- `internal/server/sse.go` — SSE handler.

Frontend additions:
- `web/src/lib/sse.ts` — single shared SSE client that publishes into React Query cache.
- `web/src/pages/Settings/Webhooks.tsx`.

## Working Rules

**HMAC is non-negotiable.** Every webhook ingress validates HMAC. Constant-time comparison. No "trust the source IP" shortcuts. The Tailscale-only network model assumes traffic is private, but signed webhook bodies stop a compromised adjacent service from forging events.

**At-most-once delivery.** The in-process bus does not retry. Reconciliation catches what was missed. Two paths into state (events for low latency, reconciliation for completeness) is intentional.

**Backpressure: drop the slow subscriber.** A subscriber that can't keep up gets unsubscribed with a logged event. Do not block publishers.

**Webhook endpoint secrets are written to `secrets`, not `webhook_endpoints`.** The `secret_token` column on `webhook_endpoints` holds the lookup key into `secrets`, never the raw token. Update `PROJECT.md` Appendix A's column semantics in `DECISIONS.md` if needed.

**The qBit external-program hook is a one-line script.** Don't ship a binary. Operators copy a `bash -c "curl -X POST ..."` template into qBittorrent's settings. Phase 4 ships the template and documents the install steps.

## What "Phase 4 Complete" Looks Like

After Phase 4 ships, the operator can:

1. Add an autobrr instance in `config/automation-tools.yaml` (or `webhook_endpoints` via UI), receive a secret token displayed once.
2. Configure autobrr to call `/webhook/autobrr/:id` on grab. Confirm with `journalctl` that the webhook receives, validates HMAC, and publishes to the bus.
3. Add the qBittorrent external-program-on-event hook from `scripts/qbit-hook-template.sh`. Confirm completion events flow through.
4. Open the torrents page; complete a torrent; see the row update within a second without a refresh (SSE-driven cache invalidation).
5. Stop qBittorrent for 20 minutes; restart. Within the next reconciliation cycle, missed completions appear in the torrent list and `audit_log` shows the reconciliation activity.
6. Visit Settings → Webhooks to rotate a token; previous token immediately rejected; new token works on the next call.

## Begin

Read `PROJECT.md §5.5`. Promote `internal/eventbus` to support multiple subscribers and typed topics. Build `internal/webhooks` next. Add the public ingress routes. Wire the SSE handler. Then write the reconciler. Update `PROGRESS.md` after each subsystem. Stop at the end of Phase 4.
