A lexicon-driven AppView for ATProto.

fix(dead-letters): admin endpoints span both legacy + new tables master

The trigger-keyed dispatcher writes failures to `dead_letter_scripts` (introduced in a086830). The admin endpoints under `/admin/dead-letters/*` were still hardcoded to read from the legacy `dead_letter_hooks` table, so: - The dashboard list / count silently dropped any failure from the new dispatcher (record-event scripts AND label scripts). - Per-id `retry` / `dismiss` / `reindex` returned 404 for any new id. Make the surface span both tables. Per-id operations route by id format (id parses as integer → `dead_letter_scripts`; anything else → `dead_letter_hooks`); the two id namespaces are disjoint (BIGSERIAL/INTEGER vs UUID/TEXT) so this dispatch is unambiguous. List / count run two queries and merge in Rust — `dead_letter_hooks` has flat columns while `dead_letter_scripts` carries event fields inside `payload` JSON, so a portable SQL UNION isn't worth the backend-specific JSON-extract dance. Schema mapping for `dead_letter_scripts` rows onto the existing `DeadLetterSummary` shape: - **Record events** (`host_kind = "record"`): collection / did / uri / rkey / action come from `payload.{collection,did,uri,rkey,action}`; `lexicon_id` reuses the collection. - **Label arrivals** (`host_kind = "label"`): `lexicon_id` is the trigger id (e.g. `labeler.apply:app.bsky.feed.post`); `did` is the labeler DID (`host_id`); `action` is the literal `"label"`; `rkey` is `payload.val`. `retry_single` / `reindex_single` reject label dead letters with a clear 400 — the upstream label event is gone, so re-running the script with the original payload is meaningful but reindexing isn't, and the user-visible error spells out which. Helpers `mark_resolved` / `update_error` now take a `DeadLetterSource` and bind the id as `i64` for the new table (sqlite implicit-conversion otherwise quietly fails to match). Bulk operations: - `all: true` updates / collects ids from both tables; the optional `collection` filter only constrains the legacy table (the new table doesn't have a collection column — filtering by collection there would require JSON-parsing the payload in SQL). - `ids: [...]` routes each id by source. No new tests added — the existing `e2e_admin` suite still passes (22/23; the 1 failure is `backfill_create_job` which is flaky on origin/dev too). Signed-off-by: Chris Pardy <chris.pardy@gmail.com>


+606 -213
1 changed file