feat(web+appview): admin theme list page — GET /admin/themes + CRUD routes (ATB-58) (#88)
* feat(appview): add GET /api/admin/themes — unfiltered theme list for admin UI (ATB-58)
* fix(appview): add cleanDatabase, isTruncated, and Bruno collection for GET /api/admin/themes (ATB-58)
* feat(appview): add POST /api/admin/themes/:rkey/duplicate — clone theme with new TID (ATB-58)
* fix(appview): use != null guards for optional fields and add cssOverrides/fontUrls test in duplicate (ATB-58)
* feat(web): add canManageThemes permission check and Themes card on admin landing (ATB-58)
* test(web): add negative assertions to admin landing page permission tests
Add missing negative assertions to ensure single-permission tests verify
that unrelated cards are not shown. The Themes card test now asserts that
members/structure/modlog links are absent; the manageCategories, moderatePosts,
banUsers, and lockTopics tests now assert that the themes link is absent.
* test(web): complete themes card assertions across all admin landing tests
Add missing `href="/admin/themes"` assertions to three tests:
- wildcard (*) permission test: assert themes card IS shown
- manageMembers-only test: assert themes card is NOT shown
- manageMembers + moderatePosts combo test: assert themes card is NOT shown
* feat(web): implement GET /admin/themes page — theme cards, policy form, create form (ATB-58)
* fix(web): rename _THEME_PRESETS and log non-404 policy fetch errors (ATB-58)
* feat(web): POST /admin/themes — create theme from preset and redirect (ATB-58)
* feat(web): POST /admin/themes/:rkey/duplicate — proxy duplicate to AppView (ATB-58)
* feat(web): POST /admin/themes/:rkey/delete — proxy delete to AppView with 409 handling (ATB-58)
* feat(web): POST /admin/theme-policy — update theme policy with availability and defaults (ATB-58)
* fix(appview): PUT /theme-policy accepts availableThemes without cid — looks up from DB (ATB-58)
* fix(web): add auth/permission/network tests and 409-specific delete handling (ATB-58)
Add missing unauthenticated, 403, and network-error tests to all four POST
theme routes. Separate the 409 branch in POST /admin/themes/:rkey/delete to
return a web-layer-owned human-friendly message. Strengthen the availableThemes
assertion in the theme-policy success test to verify exact payload shape.
* fix(atb-58): address PR review — CID validation, SyntaxError handling, Bruno seq
- Return 400 (not 200 with cid:"") when availableThemes contains uri-only entries
not found in the themes DB — empty string is not a valid AT Proto strongRef CID
- Wrap Response.json() calls in GET /admin/themes in inner try-catch so upstream
non-JSON responses are caught as parse errors rather than re-thrown as programming
errors via isProgrammingError(SyntaxError)
- Fix Bruno seq conflict: Duplicate Theme seq 4→5, List Themes seq 5→6
* fix(atb-58): block cid:\"\" as invalid strongRef; add DB failure test for needsLookup
- Introduce isMissingCid predicate (typeof !== string || === "") applied to all
three sites: needsLookup check, unresolvedUris filter, resolvedThemes map.
Explicit cid:"" bypassed the previous typeof-only guard and would have been
written verbatim to the PDS as an invalid strongRef CID.
- Add test: cid:"" entry returns 400 (same as absent cid not found in DB)
- Add test: DB select failure during needsLookup returns 500