feat(admin): bridge-avatar cache diagnostic page (LC-207) #273

Merged
longjacksonle merged 1 commit from feat/lc-207-bridge-avatar-admin-diagnostics into main 2026-05-30 03:38:36 +02:00

What

A read-only /admin/bridges/avatars page that answers "why is this bridged user showing initials" without dropping to SQL: a stats header (cached count + per-status ok/pending/failed, bytes on disk, oldest last-seen, stale-pending anomaly counter) plus the last 50 failed fetches with failure_reason and foreign host. Reached via an "Avatar cache" link on /admin/bridges.

Hybrid mirror, not a 1:1 copy

The cited precedent (/admin/outgoing-webhooks/{id}/deliveries) is a per-entity drill-in; the avatar cache is global, keyed by hash, owned by no bridge. So the page takes the table / inline-empty-state / read-only / static-on-load / AdminUser bones from deliveries, and the nav-reachable full-page (load_chrome) shape from the bridges/bots pages. Four diffs from the precedent, each forced by that data-model difference and named rather than silently diverged:

  1. Global page, not per-entity drill-in.
  2. A net-new stats header (deliveries has none).
  3. Host-only + full-URL-tooltip redaction (deliveries shows no URL, so no precedent to match).
  4. Failures-only filter (deliveries shows all rows).

Schema reality drives everything time-based

bridge_avatar_proxies has no created_at, no failed_at; fetched_at is NULL on pending/failed; only last_seen_at is non-null (insert time, bumped to now on every reference). So the failures table orders by last_seen_at DESC and "oldest" is MIN(last_seen_at), labeled honestly as "oldest last-seen" (the schema carries no creation timestamp). That column is also GC-aligned (the unreferenced sweep deletes by it) and surfaces the page's actual subjects first: a recently-referenced failure is an avatar users are actively hitting that renders as initials. Ordering by a non-existent failed_at, or by fetched_at (NULL on exactly the failed rows the page is about), would both have been subtle bugs.

Decisions

  • Host-only + tooltip: host in the scannable cell, full URL in title=. An unparseable foreign_url renders a literal <unparseable> sentinel (a truncated raw string would read like a host and hide the anomaly; the raw value still rides in the tooltip). Host-only is a summary affordance, not a secret: admin already has DB access, and the migration's "URL not in rendered HTML" side-channel concern targets room-viewer message HTML, not an admin-only page.
  • Stale-pending threshold = 4200s = sweep pending threshold (600s) + one sweep interval (3600s hourly tick). So the counter means "a full sweep opportunity passed without flipping this to failed" (sweeper broken / restart-looping), not normal lag. last_seen_at is insert-time and only bumped forward, so the counter can't over-count from an ancient reference. The two sweep constants are a bare literal arg + a function-local const, so a parity meta-test would need plumbing both out; the derivation lives in a re-derive-if-constants-change doc comment.
  • Read-only by design, matching both the precedent and the LC-78 v2 "failed is terminal" posture. The retry argument is weaker for avatars than webhooks (an avatar retry only helps if the foreign homeserver recovered, which v2 doesn't track), so the precedent's no-retry propagates for a principled reason. Retry-failed is the named LC-78-AVATAR-REFRESH follow-up.

Implementation

db::bridge_avatar_proxies::cache_stats (single-query conditional-SUM aggregate; COALESCE(SUM(byte_size),0) naturally sums ok rows only) + recent_failures, both covered by the existing (fetch_status, last_seen_at) index. No new migration. i18n keys added to en + es (the i18n_catalog parity test enforces full coverage). The whole route surface is standalone-only, so the route test is #![cfg(feature = "standalone")] gated.

Test plan

  • db-layer: recent_failures filters + orders; cache_stats counts/sums/stale-threshold; empty-cache zeros + None oldest; a 30-min pending row is NOT stale (inside the threshold).
  • route-layer: empty-state renders (no None discriminant leak); failed row shows host in cell + full URL in tooltip; unparseable URL shows the <unparseable> sentinel with raw value in tooltip (pins the sentinel against a future truncate-fallback refactor); stale-pending anomaly banner appears.
  • just check + just build-css clean; full suite green both modes (standalone + saas, 129 binaries each, zero failures).

Out of scope (held)

No retry/clear/delete actions, no new migration, no created_at/failed_at column add, no generic cache-diagnostics framework, no new layout, no top-nav entry.

🤖 Generated with Claude Code

## What A read-only `/admin/bridges/avatars` page that answers "why is this bridged user showing initials" without dropping to SQL: a stats header (cached count + per-status ok/pending/failed, bytes on disk, oldest last-seen, stale-pending anomaly counter) plus the last 50 failed fetches with `failure_reason` and foreign host. Reached via an "Avatar cache" link on `/admin/bridges`. ## Hybrid mirror, not a 1:1 copy The cited precedent (`/admin/outgoing-webhooks/{id}/deliveries`) is a per-entity drill-in; the avatar cache is global, keyed by `hash`, owned by no bridge. So the page takes the **table / inline-empty-state / read-only / static-on-load / AdminUser** bones from deliveries, and the **nav-reachable full-page (`load_chrome`)** shape from the bridges/bots pages. Four diffs from the precedent, each forced by that data-model difference and named rather than silently diverged: 1. Global page, not per-entity drill-in. 2. A net-new stats header (deliveries has none). 3. Host-only + full-URL-tooltip redaction (deliveries shows no URL, so no precedent to match). 4. Failures-only filter (deliveries shows all rows). ## Schema reality drives everything time-based `bridge_avatar_proxies` has **no `created_at`, no `failed_at`**; `fetched_at` is NULL on pending/failed; only `last_seen_at` is non-null (insert time, bumped to now on every reference). So the failures table orders by `last_seen_at DESC` and "oldest" is `MIN(last_seen_at)`, labeled honestly as "oldest last-seen" (the schema carries no creation timestamp). That column is also GC-aligned (the unreferenced sweep deletes by it) and surfaces the page's actual subjects first: a recently-referenced failure is an avatar users are actively hitting that renders as initials. Ordering by a non-existent `failed_at`, or by `fetched_at` (NULL on exactly the failed rows the page is about), would both have been subtle bugs. ## Decisions - **Host-only + tooltip:** host in the scannable cell, full URL in `title=`. An unparseable `foreign_url` renders a literal `<unparseable>` sentinel (a truncated raw string would read like a host and hide the anomaly; the raw value still rides in the tooltip). Host-only is a summary affordance, not a secret: admin already has DB access, and the migration's "URL not in rendered HTML" side-channel concern targets room-viewer message HTML, not an admin-only page. - **Stale-pending threshold = 4200s** = sweep pending threshold (600s) + one sweep interval (3600s hourly tick). So the counter means "a full sweep opportunity passed without flipping this to failed" (sweeper broken / restart-looping), not normal lag. `last_seen_at` is insert-time and only bumped forward, so the counter can't over-count from an ancient reference. The two sweep constants are a bare literal arg + a function-local const, so a parity meta-test would need plumbing both out; the derivation lives in a re-derive-if-constants-change doc comment. - **Read-only by design**, matching both the precedent and the LC-78 v2 "failed is terminal" posture. The retry argument is weaker for avatars than webhooks (an avatar retry only helps if the foreign homeserver recovered, which v2 doesn't track), so the precedent's no-retry propagates for a principled reason. Retry-failed is the named `LC-78-AVATAR-REFRESH` follow-up. ## Implementation `db::bridge_avatar_proxies::cache_stats` (single-query conditional-SUM aggregate; `COALESCE(SUM(byte_size),0)` naturally sums ok rows only) + `recent_failures`, both covered by the existing `(fetch_status, last_seen_at)` index. No new migration. i18n keys added to en + es (the `i18n_catalog` parity test enforces full coverage). The whole route surface is standalone-only, so the route test is `#![cfg(feature = "standalone")]` gated. ## Test plan - db-layer: `recent_failures` filters + orders; `cache_stats` counts/sums/stale-threshold; empty-cache zeros + `None` oldest; a 30-min pending row is NOT stale (inside the threshold). - route-layer: empty-state renders (no `None` discriminant leak); failed row shows host in cell + full URL in tooltip; unparseable URL shows the `<unparseable>` sentinel with raw value in tooltip (pins the sentinel against a future truncate-fallback refactor); stale-pending anomaly banner appears. - `just check` + `just build-css` clean; full suite green **both modes (standalone + saas, 129 binaries each, zero failures)**. ## Out of scope (held) No retry/clear/delete actions, no new migration, no `created_at`/`failed_at` column add, no generic cache-diagnostics framework, no new layout, no top-nav entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(admin): bridge-avatar cache diagnostic page (LC-207)
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Nosey parker (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 7s
Create release / Create release from merged PR (pull_request) Has been skipped
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 10s
Check / clippy + fmt + tests (pull_request) Successful in 4m15s
9ba932a15a
Adds a read-only /admin/bridges/avatars page answering "why is this bridged user showing initials" without dropping to SQL: a stats header (cached count + per-status ok/pending/failed breakdown, bytes on disk, oldest last-seen, and a stale-pending anomaly counter) plus the last 50 failed fetches with failure_reason and foreign host. Reached via an "Avatar cache" link on /admin/bridges.

Hybrid mirror, not a 1:1 copy. The cited precedent (/admin/outgoing-webhooks/{id}/deliveries) is a per-entity drill-in; the avatar cache is global, keyed by hash, owned by no bridge. So the page borrows the table / inline-empty-state / read-only / static-on-load / AdminUser shape from deliveries, but the nav-reachable full-page (load_chrome) shape from the bridges/bots pages. Four diffs from the precedent, each forced by that data-model difference and called out rather than silently diverged: (1) global page not per-entity drill-in; (2) a net-new stats header the deliveries page has no precedent for; (3) host-only-with-full-URL-tooltip redaction, a fresh choice since the deliveries page shows no URL at all; (4) failures-only filter rather than all-rows.

Schema reality drives everything time-based. bridge_avatar_proxies has no created_at and no failed_at; fetched_at is NULL on pending/failed rows; only last_seen_at is non-null (insert time, bumped to now on every reference). So the failures table orders by last_seen_at DESC and the "oldest" stat is MIN(last_seen_at) - honestly labeled "oldest last-seen", not "oldest entry", because the schema carries no creation timestamp. last_seen_at is also the GC-aligned column (the unreferenced sweep deletes by it) and surfaces the page's actual subjects first: a recently-referenced failure is an avatar users are actively hitting that renders as initials. Ordering by a non-existent failed_at, or by fetched_at (NULL on the failed rows the page is entirely about, silently excluding them), would both have been subtle bugs; verifying the schema before mirroring caught it.

Host redaction: foreign_url renders host-only in the scannable cell with the full URL in a title= tooltip (one hover away, in-DOM for copy). An unparseable foreign_url renders a literal <unparseable> sentinel rather than a truncated raw string, which would visually read like a host and hide the anomaly. Host-only is a summary affordance, not a secret: admin already has DB access, and the migration's "URL must not appear in rendered HTML" side-channel concern targets room-viewer message HTML seen by every viewer, not an admin-only diagnostic page.

Stale-pending counter: pending rows older than 4200s by last_seen_at. Derived as the sweep's pending threshold (600s) plus one sweep interval (3600s, spawn_orphan_sweeper's hourly tick), so the counter means "a full sweep opportunity passed without flipping this to failed" (sweeper broken / restart-looping) rather than normal sweep lag. last_seen_at is set at insert and only ever bumped forward, so the counter cannot over-count from an ancient reference. The two sweep constants are a bare literal arg and a function-local const, so a parity meta-test would require plumbing both out; the derivation lives in a re-derive-if-constants-change doc comment instead.

Read-only by design, matching both the deliveries precedent and the LC-78 v2 "failed is terminal, render-initials-forever" posture. The retry argument is weaker for avatars than webhooks (a webhook redelivery can still land useful; an avatar retry only helps if the foreign homeserver itself recovered, which v2 does not track), so the precedent's lack of a retry action propagates for a principled reason, not just convenience. Retry-failed is the already-named LC-78-AVATAR-REFRESH follow-up.

New db helpers (db::bridge_avatar_proxies::cache_stats single-query conditional-SUM aggregate + recent_failures), both covered by the existing (fetch_status, last_seen_at) index; no new migration. i18n keys added to en + es (the i18n_catalog parity test enforces full coverage). The whole route surface is standalone-only (routes::admin is), so the route test is file-scope #![cfg(feature = "standalone")] gated.

Tests: db-layer (recent_failures filters + orders; cache_stats counts/sums/stale-threshold; empty-cache zeros + None oldest; 30-min pending is not stale); route-layer (empty-state renders with no None discriminant leak; failed row shows host in cell + full URL in tooltip; unparseable URL shows the <unparseable> sentinel with raw value in tooltip - pins the sentinel choice against a future truncate-fallback refactor; stale-pending anomaly banner appears).

Validation: just check + just build-css clean; full suite green both modes (standalone and saas, 129 binaries each, zero failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-207-bridge-avatar-admin-diagnostics 2026-05-30 03:38:36 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!273
No description provided.