commits
Security:
- Add CSS injection guard to ThemeSwatchPreview (rejects values containing
; < }) matching the pattern already used in admin-themes.tsx
Error handling:
- Split network/JSON try blocks in preview, GET themes list, POST policy
fetch — SyntaxError from res.json() is a data error, not a code bug,
and must not be re-thrown via isProgrammingError
- Promote logger.warn → logger.error for themes list fetch failure
- Add logger.warn to preview endpoint catch block (was silently swallowing
AppView failures)
User-facing:
- Map ?error= codes to friendly messages; drop unknown codes (phishing
vector for crafted URLs showing raw internal codes like "invalid-theme")
Tests:
- Add getSetCookie absence assertions to allowUserChoice:false and
invalid-theme POST rejection tests
- Update ?error=invalid-theme GET test to verify friendly message in
settings-banner--error element
- Add tests for themes list non-ok response and network throw paths
- Add test for unknown ?error= code producing no banner
Docs:
- Align theme-resolution.ts internal section comments to use descriptive
headings instead of "Step N" (conflicted with JSDoc 5-step waterfall)
- CLAUDE.md: clarify settings routes bypass ThemeCache intentionally
Adds .claude/ to .gitignore (Claude Code local state, machine-specific).
Commits the 5-phase implementation plan and test requirements used to
implement user theme preferences.
Document the theme resolution waterfall (now 5 steps with user
preference cookies) and the cookie protocol contract for
atbb-light-theme / atbb-dark-theme in CLAUDE.md.
- [Critical] Wrap decodeURIComponent(errorParam) in try/catch to prevent URIError on malformed URLs
- [Minor] Change array type syntax from T[] to Array<T> for consistency
- [Minor] Remove duplicate POST happy-path test
All settings tests pass; build and lint succeed.
Per TypeScript house style, complex object array types should use
Array<{ uri: string }> syntax instead of { uri: string }[].
File: apps/web/src/lib/theme-resolution.ts, line 78
Function: resolveUserThemePreference()
Completed brainstorming session. Design includes:
- Cookie-based preference storage (atbb-light-theme, atbb-dark-theme)
- PRG form with HTMX live color swatch preview
- User preference as first step in theme resolution waterfall
- 5 implementation phases with full acceptance criteria
* fix: don't re-throw TypeError from fetch() in getSession as a programming error
Node.js's undici throws TypeError: fetch failed for network failures (e.g.
AppView unreachable). The previous catch block called isProgrammingError()
which classifies all TypeErrors as code bugs and re-throws them, causing
every request to return a 500 when the AppView is down.
Fix: split the fetch() call into its own try-catch in both getSession and
getSessionWithPermissions so any throw from the raw fetch — regardless of
error type — is treated as a network failure and returns gracefully.
Adds regression tests using new TypeError("fetch failed") to match the
exact error undici produces in production.
* fix: guard res.json() calls against SyntaxError from malformed AppView responses
The split-try-catch refactor (previous commit) isolated fetch() correctly but
left res.json() and permRes.json() unprotected. A proxy returning an HTML error
page on a 200 response would throw an unhandled SyntaxError, crashing the request
with no structured log at the failure site.
Wrap both .json() calls in their own try-catch blocks with specific error messages,
returning { authenticated: false } / empty permissions as appropriate.
Also: rename misleading test ("response is malformed" now clarifies it tests
missing fields, not SyntaxError), tighten TypeError assertion in
getSessionWithPermissions to verify did and error fields are logged.
* fix: theme toggle not switching between light and dark mode
Two bugs prevented the light/dark toggle from working:
1. toggleColorScheme() used `m&&m[1]==='light'` which evaluates to null
(falsy) when no cookie exists yet, causing the first toggle click to
set cookie to 'light' instead of 'dark' — a no-op since light is the
default. Fixed by extracting current scheme before toggling.
2. FALLBACK_THEME always used neobrutal-light tokens regardless of color
scheme. When no dark theme is configured in the policy, toggling to
dark changed the icon but kept light-colored tokens. Added
fallbackForScheme() that returns neobrutal-dark tokens for dark mode.
https://claude.ai/code/session_01CnyPWgayLMmPZ2Ritq2Lcj
* test: add regression coverage for toggle logic and dark-scheme fallback paths
Add pinning test for the corrected toggleColorScheme script to prevent
silent reversion to the null-evaluating m&&m[1]==='light' pattern.
Add dark-scheme network exception test for resolveTheme to verify
fallbackForScheme() returns dark tokens on all fallback paths, not just
the !policyRes.ok path that was previously the only dark-scheme test.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Node.js v22 strictly enforces ERR_IMPORT_ATTRIBUTE_MISSING for JSON imports
in ESM context. Add `with { type: "json" }` to all five preset JSON imports
in admin-themes.tsx to match the pattern already used in theme-resolution.ts.
* feat(web+appview): theme caching layer (ATB-56)
Add in-memory TTL cache for resolved theme data on the web server to
avoid redundant AppView API calls on every page request.
- New ThemeCache class (theme-cache.ts): TTL entries for policy (single)
and themes (keyed by uri:colorScheme to keep light/dark isolated)
- resolveTheme now accepts an optional ThemeCache; checks cache before
each fetch, populates after successful CID validation; stale CID on
cache hit falls through to a fresh fetch rather than serving stale data
- createThemeMiddleware creates one ThemeCache at startup (shared across
all requests); accepts configurable cacheTtlMs (default 5 min)
- THEME_CACHE_TTL_MS env var exposed via WebConfig.themeCacheTtlMs
- AppView theme endpoints now set Cache-Control: public, max-age=300;
GET /api/themes/:rkey also sets ETag from the theme record CID
* fix(web+appview): address code review feedback on theme caching (ATB-56)
Critical: Cache-Control on GET /themes was set before DB queries, causing
CDNs to cache error responses for 5 minutes. Moved to immediately before
each success return, matching the existing pattern in GET /:rkey.
Important fixes:
- Add THEME_CACHE_TTL_MS to turbo.json env array (Turbo blocks env vars
not declared here, causing tests to receive NaN TTL via turbo)
- Guard parseInt result with Number.isNaN fallback in config (invalid
env value would produce an immortal cache with no operator feedback)
- Add ThemeCache.deleteTheme(): evict stale entry when CID mismatch is
detected so failed re-fetches don't loop per-request indefinitely
- CachedTheme.tokens: Record<string,string> (was unknown) — eliminates
downstream casts and prevents numeric token values entering the cache
- Remove unused re-export of cache types from theme-resolution.ts
Suggestions applied:
- JSDoc on CachedPolicy.availableThemes[].cid explaining live vs pinned refs
- getPolicy()/getTheme() now return Readonly<T> to prevent external mutation
- Comment on ThemeCache construction in middleware explaining why it must
be outside the request handler
Test additions:
- 503 from GET /themes must NOT include Cache-Control header (regression)
- stale CID + failed fresh fetch: eviction means next request retries cleanly
- cache repopulated after stale-CID recovery: third call makes no fetches
- deleteTheme() targeted eviction tests
- Fixed misleading comment in policy-cache-hit test
* feat(web): theme import/export JSON for admin theme list page (ATB-60)
Adds GET /admin/themes/:rkey/export (JSON attachment download, excludes
cssOverrides) and POST /admin/themes/import (file upload with per-field
validation, strips unknown tokens, drops cssOverrides, delegates to
existing POST /api/admin/themes). Export and import buttons added to the
theme list page. 26 new tests covering auth, validation, and happy paths.
* refactor(web): address code review feedback on ATB-60 import/export
- Bind errors in all bare catch blocks; add isProgrammingError re-throw
in export JSON parse and parseBody catch paths
- Split uploaded.text() and JSON.parse into separate try blocks for
distinct error messages and log entries
- Add logger.error to extractAppviewError catch and parseBody catch
- Add 100 KB file size guard before reading uploaded file
- Slugify colorScheme in export filename to guard against unexpected values
- Fix route registration comment: 4-segment path is distinct from 3-segment
/:rkey — registration order does not matter
- Rewrite cssOverrides drop comment to focus on portability and CSS bleed
- Update FCIS annotation to reference project one-file-per-route-group convention
- Add safety comment on fontUrls cast (isHttpsUrl verifies typeof === "string")
- Add tests: non-404 AppView error → 500, fontUrls non-array, AppView POST
network failure; change mockFetch.mock.calls[N] to .at(-1)! with URL assertion
Adds a cookie-based color scheme toggle button to the site header (both
desktop and mobile navs). Clicking it flips the atbb-color-scheme cookie
between light/dark and reloads the page so the server re-renders with the
correct preset tokens resolved by ATB-53's theme middleware.
- NavContent now accepts colorScheme and renders a toggle button with a
contextual aria-label ("Switch to dark mode" / "Switch to light mode")
- toggleColorScheme() vanilla JS sets cookie (path=/, max-age=1yr,
SameSite=Lax) and calls location.reload()
- .color-scheme-toggle CSS class follows neobrutal button aesthetics
- 5 new tests cover button presence, aria-label for both modes, dual-nav
rendering, onclick wiring, and cookie attribute correctness
* feat(theming): ship built-in preset themes with canonical atbb.space refs (ATB-61)
Redesigns themeRef to use optional CID (live vs pinned refs), ships 5 complete
preset token sets, and adds release pipeline + escape-hatch scripts.
Lexicon:
- themePolicy#themeRef: replace strongRef wrapper with flat { uri, cid? }
uri-only = live ref (auto-updates); uri+cid = pinned ref (version-locked)
DB:
- theme_policy_available_themes.theme_cid: DROP NOT NULL (migration 0014)
Presets:
- Add clean-light.json, clean-dark.json, classic-bb.json (3 new presets)
- All 5 presets bundled as hardcoded fallback + deployment pipeline source
AppView:
- indexer: update themeRef field access (.theme.uri -> .uri, .theme.cid -> .cid)
- admin PUT /api/admin/theme-policy: remove CID-autofill/DB-lookup block;
pass CID through when provided, omit when absent; flat PDS record write
- theme-resolution: cid optional in ThemePolicyResponse type; split warning
for "URI not in availableThemes" vs "absent CID on live ref"
Scripts:
- publish-presets.ts: idempotent release pipeline script; skips unchanged
presets by comparing sorted token JSON; preserves createdAt on updates
- bootstrap-local-presets.ts: escape-hatch for zero-external-deps installs;
writes presets to forum's own PDS, rewrites themePolicy to local URIs
Docs:
- theming-plan.md: document canonical-presets design, live/pinned ref model,
deployment pipeline, local escape hatch, updated resolution waterfall
- ATB-61 Linear issue updated with new scope and acceptance criteria
* fix(atb-61): address PR review feedback on preset theme implementation
- Fix rkey regex to allow hyphens (/^[a-z0-9-]+$/i) — all 5 preset rkeys
contain hyphens (neobrutal-light, clean-dark, etc.) and were silently
falling back to the hardcoded theme
- Add live-ref resolveTheme test verifying CID check is skipped when
expectedCid is null (canonical atbb.space presets ship without CID)
- Add ThemePolicy indexer tests verifying flat .uri field access and
null themeCid for live refs
- Fix bare catch in publish-presets.ts to only swallow 404 (record not
found) and rethrow all other errors
- Add isRecordCurrent() helper including name/colorScheme in change
detection, not just tokens
- Replace non-null assertions on .find() in admin.ts with explicit guards
- Log existing themePolicy before overwriting in bootstrap-local-presets.ts
- Update Bruno Update Theme Policy docs: remove stale CID-lookup comment
* test(css-sanitizer): prove @IMPORT and EXPRESSION() case-insensitive handling
The sanitizer uses .toLowerCase() before comparing atrule/function names, so
uppercase obfuscation variants are already caught. These two tests document
that assumption explicitly so future changes can't silently break it.
* refactor(cli): move theme preset scripts into atbb CLI as theme subcommands
Moves `publish-presets.ts` and `bootstrap-local-presets.ts` from
`apps/appview/scripts/` into the `@atbb/cli` package as
`atbb theme publish-canonical` and `atbb theme bootstrap-local`.
Both commands sit alongside the existing init/category/board commands
and reuse the CLI's ForumAgent auth infrastructure. The appview npm
scripts that wrapped the old scripts are removed.
* feat(web+appview): CSS sanitization for theme cssOverrides (ATB-62)
Add @atbb/css-sanitizer workspace package (css-tree v2 AST-based) that
strips dangerous CSS constructs — @import, external url(), @font-face
with external src, expression(), -moz-binding, behavior, data: URIs —
while preserving safe structural overrides.
- appview: sanitize cssOverrides at write time (POST + PUT /api/admin/themes)
and log any stripped constructs as structured warnings
- web: replace inline stub sanitizeCss with the real package; enable the
CSS overrides textarea in the theme editor (was disabled pending ATB-62)
* fix(css-sanitizer): address PR review security and quality issues
Critical:
- Strip </style> sequences from generated output to prevent HTML parser
breakout when CSS is injected via dangerouslySetInnerHTML (XSS regression)
- Fail closed on css-tree onParseError: Raw nodes from error recovery bypass
walker checks, so discard entire output when any parse error occurs
- Wrap sanitizeCssOverrides calls in dedicated try-catch in POST and PUT
theme handlers (separate from PDS write block per CLAUDE.md granularity rule)
- Add try-catch around sanitizeCss calls in BaseLayout with empty fallback
so a css-tree bug doesn't 500 every page for all users
Security:
- Sanitize cssOverrides in POST /api/admin/themes/:rkey/duplicate so
pre-sanitization records don't propagate dangerous CSS via duplication
- Move warning push after list.remove() so audit log only says "Stripped X"
when the node was actually removed (not before the null-check)
- Fix onParseError type signature: (error: SyntaxError) => void
Quality:
- Replace JSON.stringify(warnings) with warnings in structured logger calls
- Update Bruno Create Theme.bru: remove stale ATB-62 placeholder text
- Add integration tests: dangerous CSS stripped in POST and PUT theme handlers
- Fix duplicate test expectation: sanitizer now runs on duplication (compact form)
- Fix </style> test: split into fail-closed test and string-literal stripping test
* docs: add design doc for ATB-59 admin theme token editor
Covers file structure (extract to admin-themes.tsx), editor page layout,
HTMX preview endpoint, save/reset flows, error handling, and test plan.
* docs: add implementation plan for ATB-59 theme token editor
Covers extract-to-admin-themes.tsx, TDD for GET /admin/themes/:rkey,
preview endpoint, save, and reset-to-preset handlers.
* docs: add design doc for ATB-53 theme resolution and server-side token injection
* docs: add implementation plan for ATB-53 theme resolution and server-side token injection
* feat(appview): include cid in GET /api/themes/:rkey response (ATB-53)
* feat(web): add ResolvedTheme types, FALLBACK_THEME, and color scheme helpers (ATB-53)
* feat(web): implement resolveTheme waterfall with CID integrity check (ATB-53)
* test(web): add missing resolveTheme branch test for malformed theme URI (ATB-53)
* fix(web): re-throw programming errors in resolveTheme catch block (ATB-53)
* feat(web): add createThemeMiddleware Hono middleware (ATB-53)
* feat(web): register createThemeMiddleware on webRoutes (ATB-53)
* feat(web): BaseLayout accepts resolvedTheme prop, adds Accept-CH meta (ATB-53)
Update BaseLayout to take a required resolvedTheme prop that drives dynamic :root CSS token injection, font URL rendering, and optional cssOverrides. Remove hardcoded neobrutal-light preset import and static ROOT_CSS constant. Add Accept-CH meta tag for color scheme client hint. Update all route factories to read theme from context (falling back to FALLBACK_THEME when middleware is absent, e.g. in tests).
* fix(web): sanitize cssOverrides before injection, add null branch tests (ATB-53)
* feat(web): type auth and mod route factories with WebAppEnv (ATB-53)
* docs: move ATB-53 plan docs to complete/
* docs(bruno): update GET /api/themes/:rkey to document cid field (ATB-53)
* feat(web): thread resolvedTheme through admin-themes route factory (ATB-53)
* fix(web): address PR review — sanitize tokens, split try blocks, add logs, rkey validation (ATB-53)
- Change `import { WebAppEnv }` to `import type` in routes/index.ts (type-only import)
- Freeze FALLBACK_THEME and its fontUrls array to prevent mutation across callers
- Split single giant try block in resolveTheme into 6 focused blocks (policy fetch, policy parse, URI/rkey extraction, theme fetch, theme parse, CID check) with per-operation error messages
- Add rkey validation against /^[a-z0-9]+$/i before using in fetch URL (path traversal prevention)
- Log warning when theme URI is absent from availableThemes (CID check bypassed)
- Log warn with status+url on non-ok policy/theme responses instead of silent fallback
- SyntaxError from Response.json() is now caught as a data error and not re-thrown
- Fix detectColorScheme cookie regex to use (?:^|;\s*) prefix anchor (prevents x-atbb-color-scheme=dark from matching)
- Wrap :root token block in sanitizeCss() in base.tsx
- Filter fontUrls to https:// only before rendering link tags in base.tsx
- Add try-catch error boundary in createThemeMiddleware so unexpected throws use FALLBACK_THEME
- Add tests: invalid JSON in policy/theme responses, CID bypass warning, invalid rkey, cookie regex prefix fix, middleware error boundary, non-https font URL filtering
* docs: add design doc for ATB-59 admin theme token editor
Covers file structure (extract to admin-themes.tsx), editor page layout,
HTMX preview endpoint, save/reset flows, error handling, and test plan.
* docs: add implementation plan for ATB-59 theme token editor
Covers extract-to-admin-themes.tsx, TDD for GET /admin/themes/:rkey,
preview endpoint, save, and reset-to-preset handlers.
* refactor(web): extract theme admin handlers into admin-themes.tsx (ATB-59)
* refactor(web): mount admin-themes routes, remove extracted code from admin.tsx (ATB-59)
* test(web): add failing tests for GET /admin/themes/:rkey (ATB-59)
* test(web): improve admin-themes test quality for GET /admin/themes/:rkey
- Extract MANAGE_THEMES constant to reduce repetition
- Rename setupAuth → setupAuthenticatedSession to match admin.test.tsx pattern
- Remove unnecessary fetch mock from unauthenticated test
- Strengthen CSS overrides assertion to require co-location via regex
- Add colorScheme and second token assertions to happy-path test
- Restore strict "Access Denied" assertion on 403 test
- Add ATB-62 reference to CSS overrides test description
* feat(web): GET /admin/themes/:rkey token editor page + fix Edit button (ATB-59)
* fix(web): block } in sanitizeTokenValue to prevent CSS block-escape injection (ATB-59)
* test(web): write failing tests for POST /admin/themes/:rkey/preview (ATB-59 TDD red)
* test(web): strengthen preview tests — add fallback test, fix semicolon sanitization assertion (ATB-59)
* test(web): fix preview test quality — align auth fixture, strengthen } assertion, clarify description (ATB-59)
* test(web): add 403 test for preview POST — manageThemes permission gate (ATB-59)
* feat(web): POST /admin/themes/:rkey/preview — HTMX live preview endpoint (ATB-59)
Adds the live-preview fragment endpoint used by the theme editor's HTMX
integration. Sanitizes token values via sanitizeTokenValue() before
rendering ThemePreviewContent, dropping any value containing '<', ';',
or '}' to prevent CSS injection.
* fix(web): tighten sanitization assertions to --name: format, restore var(--color-bg) in preview template (ATB-59)
* test(web): write failing tests for POST /admin/themes/:rkey/save (ATB-59)
* feat(web): POST /admin/themes/:rkey/save — persist token edits to AppView (ATB-59)
* fix(web): sanitize token values on save + add PUT body forwarding test (ATB-59)
* test(web): write failing tests for POST /admin/themes/:rkey/reset-to-preset (ATB-59)
* test(web): strengthen reset-to-preset 400 assertion (ATB-59)
* feat(web): POST /admin/themes/:rkey/reset-to-preset (ATB-59)
* fix(web): address code review issues — ATB-59
- Fix GET /admin/themes/:rkey to call public /api/themes/:rkey instead
of nonexistent /api/admin/themes/:rkey; remove unused cookie variable
- Validate name before AppView PUT in save handler; redirect with error
if empty (prevents wasteful round-trip and unclear AppView message)
- Replace c.json() with redirect-on-error in reset-to-preset handler so
browser form POSTs show friendly error pages instead of raw JSON
- Add network failure test for GET /admin/themes/:rkey (500 unavailable)
- Add empty-name validation test for save handler
- Move ATB-59 plan docs to docs/plans/complete/
* docs: move ATB-59 plan docs to complete/
Break the 675-line monolithic helpers file into three focused modules:
- helpers/serialize.ts — serialization functions and DB row type aliases
- helpers/validate.ts — input validation and parameter parsing
- helpers/queries.ts — database query helpers (bans, mod status, etc.)
helpers.ts becomes a barrel re-export, so zero consumer changes needed.
This reduces merge conflicts since team members working on admin routes
won't collide with changes to serialization or query helpers.
Also fix admin modlog route: replace drizzle-orm aliased self-joins
(which generate invalid SQL for SQLite) with a batch handle lookup.
This fixes 9 pre-existing test failures in the modlog endpoint.
https://claude.ai/code/session_0119eQacx3ejToSd9c6QEc98
Co-authored-by: Claude <noreply@anthropic.com>
* 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
* docs: add design doc for ATB-57 theme write API endpoints
* docs: add implementation plan for ATB-57 theme write API endpoints
* feat(appview): add manageThemes permission to Admin role (ATB-57)
* feat(appview): POST /api/admin/themes — create theme on Forum PDS (ATB-57)
* test(appview): add 401/403/PDS-failure tests for POST /api/admin/themes (ATB-57)
* feat(appview): PUT /api/admin/themes/:rkey — update theme on Forum PDS (ATB-57)
* feat(appview): DELETE /api/admin/themes/:rkey — delete theme, 409 if default (ATB-57)
* test(appview): assert 409 error body in dark-theme default check (ATB-57)
* test(appview): verify deleteRecord called with exact theme args (ATB-57)
* feat(appview): PUT /api/admin/theme-policy — upsert policy singleton on Forum PDS (ATB-57)
* test(appview): add theme-policy update path and updatedAt assertions (ATB-57)
* refactor(appview): strengthen theme-policy type guards and test assertions (ATB-57)
* docs(bruno): add Admin Themes collection for ATB-57 write endpoints
* docs: mark ATB-57 plan docs complete, move to docs/plans/complete/
* fix(appview): add 503 ForumAgent-not-authenticated tests; fix Bruno error code docs (ATB-57)
* docs: add ATB-55 theme read API design doc
Records approved design for themes table, theme_policies table,
theme_policy_available_themes join table, firehose indexer configs,
and GET /api/themes + GET /api/themes/:rkey + GET /api/theme-policy endpoints.
* docs: add ATB-55 theme API implementation plan
* feat(db): add themes, theme_policies, theme_policy_available_themes tables
Generate Postgres (0013) and SQLite (0001) migrations for the three new
theme tables. Build @atbb/db to verify schema compiles correctly.
* feat(appview): add GET /api/themes, /api/themes/:rkey, /api/theme-policy endpoints (ATB-55)
* feat(appview): index space.atbb.forum.theme and themePolicy from firehose (ATB-55)
* docs(bruno): add Themes API collection (ATB-55)
* docs: ATB-52 CSS token extraction design doc
* docs: ATB-52 implementation plan
* test(web): add failing preset completeness tests (ATB-52)
* test(web): improve preset test descriptions (ATB-52)
* feat(web): add neobrutal-light and neobrutal-dark JSON presets with font-size-xs token (ATB-52)
* feat(web): switch base layout to JSON preset import, remove TS preset (ATB-52)
* fix(web): replace all hardcoded CSS values with design tokens in mod and structure UI (ATB-52)
Define two new AT Proto record types for the theming system:
- space.atbb.forum.theme (tid key) — design tokens, color scheme, CSS overrides, font URLs
- space.atbb.forum.themePolicy (literal:self) — available themes, light/dark defaults, user choice toggle
Uses knownValues for colorScheme extensibility and strongRef wrapped in themeRef named def for CID integrity.
README was missing 3 of 5 packages (atproto, cli, logger).
Deployment guide had SESSION_TTL_DAYS default wrong (30 vs actual 7)
and was missing LOG_LEVEL, SEED_DEFAULT_ROLES, DEFAULT_MEMBER_ROLE
from the optional env vars table. Production env example was missing
FORUM_HANDLE and FORUM_PASSWORD variables.
* feat(web): admin mod action log page — /admin/modlog (ATB-48)
* docs: ATB-48 modlog UI implementation plan and completion notes
* fix(web): wrap modlogRes.json() in try-catch for non-JSON AppView responses (ATB-48)
A proxy returning HTML with HTTP 200 would cause Response.json() to throw
SyntaxError, which isProgrammingError() re-throws, producing an unhandled crash
instead of a 500 error page. Wrap with the same pattern used in the members
and role-assignment handlers.
Step-by-step TDD plan for GET /api/admin/modlog — requireAnyPermission
middleware, Drizzle alias double join, route handler, and Bruno collection.
Design for GET /api/admin/modlog — paginated mod action audit log with
double users join for moderator and subject handles, and requireAnyPermission
middleware for OR-based permission checks.
* docs: ATB-46 mod action log endpoint design
Design for GET /api/admin/modlog — paginated mod action audit log with
double users join for moderator and subject handles, and requireAnyPermission
middleware for OR-based permission checks.
* docs: ATB-46 mod action log implementation plan
Step-by-step TDD plan for GET /api/admin/modlog — requireAnyPermission
middleware, Drizzle alias double join, route handler, and Bruno collection.
* feat(appview): add requireAnyPermission middleware (ATB-46)
* test(appview): failing tests for GET /api/admin/modlog (ATB-46)
* feat(appview): GET /api/admin/modlog with double users leftJoin (ATB-46)
* docs(bruno): GET /api/admin/modlog collection (ATB-46)
* docs: move ATB-46 plan docs to complete/
* fix(appview): scope modlog queries to forumDid (ATB-46)
Security:
- Add CSS injection guard to ThemeSwatchPreview (rejects values containing
; < }) matching the pattern already used in admin-themes.tsx
Error handling:
- Split network/JSON try blocks in preview, GET themes list, POST policy
fetch — SyntaxError from res.json() is a data error, not a code bug,
and must not be re-thrown via isProgrammingError
- Promote logger.warn → logger.error for themes list fetch failure
- Add logger.warn to preview endpoint catch block (was silently swallowing
AppView failures)
User-facing:
- Map ?error= codes to friendly messages; drop unknown codes (phishing
vector for crafted URLs showing raw internal codes like "invalid-theme")
Tests:
- Add getSetCookie absence assertions to allowUserChoice:false and
invalid-theme POST rejection tests
- Update ?error=invalid-theme GET test to verify friendly message in
settings-banner--error element
- Add tests for themes list non-ok response and network throw paths
- Add test for unknown ?error= code producing no banner
Docs:
- Align theme-resolution.ts internal section comments to use descriptive
headings instead of "Step N" (conflicted with JSDoc 5-step waterfall)
- CLAUDE.md: clarify settings routes bypass ThemeCache intentionally
* fix: don't re-throw TypeError from fetch() in getSession as a programming error
Node.js's undici throws TypeError: fetch failed for network failures (e.g.
AppView unreachable). The previous catch block called isProgrammingError()
which classifies all TypeErrors as code bugs and re-throws them, causing
every request to return a 500 when the AppView is down.
Fix: split the fetch() call into its own try-catch in both getSession and
getSessionWithPermissions so any throw from the raw fetch — regardless of
error type — is treated as a network failure and returns gracefully.
Adds regression tests using new TypeError("fetch failed") to match the
exact error undici produces in production.
* fix: guard res.json() calls against SyntaxError from malformed AppView responses
The split-try-catch refactor (previous commit) isolated fetch() correctly but
left res.json() and permRes.json() unprotected. A proxy returning an HTML error
page on a 200 response would throw an unhandled SyntaxError, crashing the request
with no structured log at the failure site.
Wrap both .json() calls in their own try-catch blocks with specific error messages,
returning { authenticated: false } / empty permissions as appropriate.
Also: rename misleading test ("response is malformed" now clarifies it tests
missing fields, not SyntaxError), tighten TypeError assertion in
getSessionWithPermissions to verify did and error fields are logged.
* fix: theme toggle not switching between light and dark mode
Two bugs prevented the light/dark toggle from working:
1. toggleColorScheme() used `m&&m[1]==='light'` which evaluates to null
(falsy) when no cookie exists yet, causing the first toggle click to
set cookie to 'light' instead of 'dark' — a no-op since light is the
default. Fixed by extracting current scheme before toggling.
2. FALLBACK_THEME always used neobrutal-light tokens regardless of color
scheme. When no dark theme is configured in the policy, toggling to
dark changed the icon but kept light-colored tokens. Added
fallbackForScheme() that returns neobrutal-dark tokens for dark mode.
https://claude.ai/code/session_01CnyPWgayLMmPZ2Ritq2Lcj
* test: add regression coverage for toggle logic and dark-scheme fallback paths
Add pinning test for the corrected toggleColorScheme script to prevent
silent reversion to the null-evaluating m&&m[1]==='light' pattern.
Add dark-scheme network exception test for resolveTheme to verify
fallbackForScheme() returns dark tokens on all fallback paths, not just
the !policyRes.ok path that was previously the only dark-scheme test.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(web+appview): theme caching layer (ATB-56)
Add in-memory TTL cache for resolved theme data on the web server to
avoid redundant AppView API calls on every page request.
- New ThemeCache class (theme-cache.ts): TTL entries for policy (single)
and themes (keyed by uri:colorScheme to keep light/dark isolated)
- resolveTheme now accepts an optional ThemeCache; checks cache before
each fetch, populates after successful CID validation; stale CID on
cache hit falls through to a fresh fetch rather than serving stale data
- createThemeMiddleware creates one ThemeCache at startup (shared across
all requests); accepts configurable cacheTtlMs (default 5 min)
- THEME_CACHE_TTL_MS env var exposed via WebConfig.themeCacheTtlMs
- AppView theme endpoints now set Cache-Control: public, max-age=300;
GET /api/themes/:rkey also sets ETag from the theme record CID
* fix(web+appview): address code review feedback on theme caching (ATB-56)
Critical: Cache-Control on GET /themes was set before DB queries, causing
CDNs to cache error responses for 5 minutes. Moved to immediately before
each success return, matching the existing pattern in GET /:rkey.
Important fixes:
- Add THEME_CACHE_TTL_MS to turbo.json env array (Turbo blocks env vars
not declared here, causing tests to receive NaN TTL via turbo)
- Guard parseInt result with Number.isNaN fallback in config (invalid
env value would produce an immortal cache with no operator feedback)
- Add ThemeCache.deleteTheme(): evict stale entry when CID mismatch is
detected so failed re-fetches don't loop per-request indefinitely
- CachedTheme.tokens: Record<string,string> (was unknown) — eliminates
downstream casts and prevents numeric token values entering the cache
- Remove unused re-export of cache types from theme-resolution.ts
Suggestions applied:
- JSDoc on CachedPolicy.availableThemes[].cid explaining live vs pinned refs
- getPolicy()/getTheme() now return Readonly<T> to prevent external mutation
- Comment on ThemeCache construction in middleware explaining why it must
be outside the request handler
Test additions:
- 503 from GET /themes must NOT include Cache-Control header (regression)
- stale CID + failed fresh fetch: eviction means next request retries cleanly
- cache repopulated after stale-CID recovery: third call makes no fetches
- deleteTheme() targeted eviction tests
- Fixed misleading comment in policy-cache-hit test
* feat(web): theme import/export JSON for admin theme list page (ATB-60)
Adds GET /admin/themes/:rkey/export (JSON attachment download, excludes
cssOverrides) and POST /admin/themes/import (file upload with per-field
validation, strips unknown tokens, drops cssOverrides, delegates to
existing POST /api/admin/themes). Export and import buttons added to the
theme list page. 26 new tests covering auth, validation, and happy paths.
* refactor(web): address code review feedback on ATB-60 import/export
- Bind errors in all bare catch blocks; add isProgrammingError re-throw
in export JSON parse and parseBody catch paths
- Split uploaded.text() and JSON.parse into separate try blocks for
distinct error messages and log entries
- Add logger.error to extractAppviewError catch and parseBody catch
- Add 100 KB file size guard before reading uploaded file
- Slugify colorScheme in export filename to guard against unexpected values
- Fix route registration comment: 4-segment path is distinct from 3-segment
/:rkey — registration order does not matter
- Rewrite cssOverrides drop comment to focus on portability and CSS bleed
- Update FCIS annotation to reference project one-file-per-route-group convention
- Add safety comment on fontUrls cast (isHttpsUrl verifies typeof === "string")
- Add tests: non-404 AppView error → 500, fontUrls non-array, AppView POST
network failure; change mockFetch.mock.calls[N] to .at(-1)! with URL assertion
Adds a cookie-based color scheme toggle button to the site header (both
desktop and mobile navs). Clicking it flips the atbb-color-scheme cookie
between light/dark and reloads the page so the server re-renders with the
correct preset tokens resolved by ATB-53's theme middleware.
- NavContent now accepts colorScheme and renders a toggle button with a
contextual aria-label ("Switch to dark mode" / "Switch to light mode")
- toggleColorScheme() vanilla JS sets cookie (path=/, max-age=1yr,
SameSite=Lax) and calls location.reload()
- .color-scheme-toggle CSS class follows neobrutal button aesthetics
- 5 new tests cover button presence, aria-label for both modes, dual-nav
rendering, onclick wiring, and cookie attribute correctness
* feat(theming): ship built-in preset themes with canonical atbb.space refs (ATB-61)
Redesigns themeRef to use optional CID (live vs pinned refs), ships 5 complete
preset token sets, and adds release pipeline + escape-hatch scripts.
Lexicon:
- themePolicy#themeRef: replace strongRef wrapper with flat { uri, cid? }
uri-only = live ref (auto-updates); uri+cid = pinned ref (version-locked)
DB:
- theme_policy_available_themes.theme_cid: DROP NOT NULL (migration 0014)
Presets:
- Add clean-light.json, clean-dark.json, classic-bb.json (3 new presets)
- All 5 presets bundled as hardcoded fallback + deployment pipeline source
AppView:
- indexer: update themeRef field access (.theme.uri -> .uri, .theme.cid -> .cid)
- admin PUT /api/admin/theme-policy: remove CID-autofill/DB-lookup block;
pass CID through when provided, omit when absent; flat PDS record write
- theme-resolution: cid optional in ThemePolicyResponse type; split warning
for "URI not in availableThemes" vs "absent CID on live ref"
Scripts:
- publish-presets.ts: idempotent release pipeline script; skips unchanged
presets by comparing sorted token JSON; preserves createdAt on updates
- bootstrap-local-presets.ts: escape-hatch for zero-external-deps installs;
writes presets to forum's own PDS, rewrites themePolicy to local URIs
Docs:
- theming-plan.md: document canonical-presets design, live/pinned ref model,
deployment pipeline, local escape hatch, updated resolution waterfall
- ATB-61 Linear issue updated with new scope and acceptance criteria
* fix(atb-61): address PR review feedback on preset theme implementation
- Fix rkey regex to allow hyphens (/^[a-z0-9-]+$/i) — all 5 preset rkeys
contain hyphens (neobrutal-light, clean-dark, etc.) and were silently
falling back to the hardcoded theme
- Add live-ref resolveTheme test verifying CID check is skipped when
expectedCid is null (canonical atbb.space presets ship without CID)
- Add ThemePolicy indexer tests verifying flat .uri field access and
null themeCid for live refs
- Fix bare catch in publish-presets.ts to only swallow 404 (record not
found) and rethrow all other errors
- Add isRecordCurrent() helper including name/colorScheme in change
detection, not just tokens
- Replace non-null assertions on .find() in admin.ts with explicit guards
- Log existing themePolicy before overwriting in bootstrap-local-presets.ts
- Update Bruno Update Theme Policy docs: remove stale CID-lookup comment
* test(css-sanitizer): prove @IMPORT and EXPRESSION() case-insensitive handling
The sanitizer uses .toLowerCase() before comparing atrule/function names, so
uppercase obfuscation variants are already caught. These two tests document
that assumption explicitly so future changes can't silently break it.
* refactor(cli): move theme preset scripts into atbb CLI as theme subcommands
Moves `publish-presets.ts` and `bootstrap-local-presets.ts` from
`apps/appview/scripts/` into the `@atbb/cli` package as
`atbb theme publish-canonical` and `atbb theme bootstrap-local`.
Both commands sit alongside the existing init/category/board commands
and reuse the CLI's ForumAgent auth infrastructure. The appview npm
scripts that wrapped the old scripts are removed.
* feat(web+appview): CSS sanitization for theme cssOverrides (ATB-62)
Add @atbb/css-sanitizer workspace package (css-tree v2 AST-based) that
strips dangerous CSS constructs — @import, external url(), @font-face
with external src, expression(), -moz-binding, behavior, data: URIs —
while preserving safe structural overrides.
- appview: sanitize cssOverrides at write time (POST + PUT /api/admin/themes)
and log any stripped constructs as structured warnings
- web: replace inline stub sanitizeCss with the real package; enable the
CSS overrides textarea in the theme editor (was disabled pending ATB-62)
* fix(css-sanitizer): address PR review security and quality issues
Critical:
- Strip </style> sequences from generated output to prevent HTML parser
breakout when CSS is injected via dangerouslySetInnerHTML (XSS regression)
- Fail closed on css-tree onParseError: Raw nodes from error recovery bypass
walker checks, so discard entire output when any parse error occurs
- Wrap sanitizeCssOverrides calls in dedicated try-catch in POST and PUT
theme handlers (separate from PDS write block per CLAUDE.md granularity rule)
- Add try-catch around sanitizeCss calls in BaseLayout with empty fallback
so a css-tree bug doesn't 500 every page for all users
Security:
- Sanitize cssOverrides in POST /api/admin/themes/:rkey/duplicate so
pre-sanitization records don't propagate dangerous CSS via duplication
- Move warning push after list.remove() so audit log only says "Stripped X"
when the node was actually removed (not before the null-check)
- Fix onParseError type signature: (error: SyntaxError) => void
Quality:
- Replace JSON.stringify(warnings) with warnings in structured logger calls
- Update Bruno Create Theme.bru: remove stale ATB-62 placeholder text
- Add integration tests: dangerous CSS stripped in POST and PUT theme handlers
- Fix duplicate test expectation: sanitizer now runs on duplication (compact form)
- Fix </style> test: split into fail-closed test and string-literal stripping test
* docs: add design doc for ATB-59 admin theme token editor
Covers file structure (extract to admin-themes.tsx), editor page layout,
HTMX preview endpoint, save/reset flows, error handling, and test plan.
* docs: add implementation plan for ATB-59 theme token editor
Covers extract-to-admin-themes.tsx, TDD for GET /admin/themes/:rkey,
preview endpoint, save, and reset-to-preset handlers.
* docs: add design doc for ATB-53 theme resolution and server-side token injection
* docs: add implementation plan for ATB-53 theme resolution and server-side token injection
* feat(appview): include cid in GET /api/themes/:rkey response (ATB-53)
* feat(web): add ResolvedTheme types, FALLBACK_THEME, and color scheme helpers (ATB-53)
* feat(web): implement resolveTheme waterfall with CID integrity check (ATB-53)
* test(web): add missing resolveTheme branch test for malformed theme URI (ATB-53)
* fix(web): re-throw programming errors in resolveTheme catch block (ATB-53)
* feat(web): add createThemeMiddleware Hono middleware (ATB-53)
* feat(web): register createThemeMiddleware on webRoutes (ATB-53)
* feat(web): BaseLayout accepts resolvedTheme prop, adds Accept-CH meta (ATB-53)
Update BaseLayout to take a required resolvedTheme prop that drives dynamic :root CSS token injection, font URL rendering, and optional cssOverrides. Remove hardcoded neobrutal-light preset import and static ROOT_CSS constant. Add Accept-CH meta tag for color scheme client hint. Update all route factories to read theme from context (falling back to FALLBACK_THEME when middleware is absent, e.g. in tests).
* fix(web): sanitize cssOverrides before injection, add null branch tests (ATB-53)
* feat(web): type auth and mod route factories with WebAppEnv (ATB-53)
* docs: move ATB-53 plan docs to complete/
* docs(bruno): update GET /api/themes/:rkey to document cid field (ATB-53)
* feat(web): thread resolvedTheme through admin-themes route factory (ATB-53)
* fix(web): address PR review — sanitize tokens, split try blocks, add logs, rkey validation (ATB-53)
- Change `import { WebAppEnv }` to `import type` in routes/index.ts (type-only import)
- Freeze FALLBACK_THEME and its fontUrls array to prevent mutation across callers
- Split single giant try block in resolveTheme into 6 focused blocks (policy fetch, policy parse, URI/rkey extraction, theme fetch, theme parse, CID check) with per-operation error messages
- Add rkey validation against /^[a-z0-9]+$/i before using in fetch URL (path traversal prevention)
- Log warning when theme URI is absent from availableThemes (CID check bypassed)
- Log warn with status+url on non-ok policy/theme responses instead of silent fallback
- SyntaxError from Response.json() is now caught as a data error and not re-thrown
- Fix detectColorScheme cookie regex to use (?:^|;\s*) prefix anchor (prevents x-atbb-color-scheme=dark from matching)
- Wrap :root token block in sanitizeCss() in base.tsx
- Filter fontUrls to https:// only before rendering link tags in base.tsx
- Add try-catch error boundary in createThemeMiddleware so unexpected throws use FALLBACK_THEME
- Add tests: invalid JSON in policy/theme responses, CID bypass warning, invalid rkey, cookie regex prefix fix, middleware error boundary, non-https font URL filtering
* docs: add design doc for ATB-59 admin theme token editor
Covers file structure (extract to admin-themes.tsx), editor page layout,
HTMX preview endpoint, save/reset flows, error handling, and test plan.
* docs: add implementation plan for ATB-59 theme token editor
Covers extract-to-admin-themes.tsx, TDD for GET /admin/themes/:rkey,
preview endpoint, save, and reset-to-preset handlers.
* refactor(web): extract theme admin handlers into admin-themes.tsx (ATB-59)
* refactor(web): mount admin-themes routes, remove extracted code from admin.tsx (ATB-59)
* test(web): add failing tests for GET /admin/themes/:rkey (ATB-59)
* test(web): improve admin-themes test quality for GET /admin/themes/:rkey
- Extract MANAGE_THEMES constant to reduce repetition
- Rename setupAuth → setupAuthenticatedSession to match admin.test.tsx pattern
- Remove unnecessary fetch mock from unauthenticated test
- Strengthen CSS overrides assertion to require co-location via regex
- Add colorScheme and second token assertions to happy-path test
- Restore strict "Access Denied" assertion on 403 test
- Add ATB-62 reference to CSS overrides test description
* feat(web): GET /admin/themes/:rkey token editor page + fix Edit button (ATB-59)
* fix(web): block } in sanitizeTokenValue to prevent CSS block-escape injection (ATB-59)
* test(web): write failing tests for POST /admin/themes/:rkey/preview (ATB-59 TDD red)
* test(web): strengthen preview tests — add fallback test, fix semicolon sanitization assertion (ATB-59)
* test(web): fix preview test quality — align auth fixture, strengthen } assertion, clarify description (ATB-59)
* test(web): add 403 test for preview POST — manageThemes permission gate (ATB-59)
* feat(web): POST /admin/themes/:rkey/preview — HTMX live preview endpoint (ATB-59)
Adds the live-preview fragment endpoint used by the theme editor's HTMX
integration. Sanitizes token values via sanitizeTokenValue() before
rendering ThemePreviewContent, dropping any value containing '<', ';',
or '}' to prevent CSS injection.
* fix(web): tighten sanitization assertions to --name: format, restore var(--color-bg) in preview template (ATB-59)
* test(web): write failing tests for POST /admin/themes/:rkey/save (ATB-59)
* feat(web): POST /admin/themes/:rkey/save — persist token edits to AppView (ATB-59)
* fix(web): sanitize token values on save + add PUT body forwarding test (ATB-59)
* test(web): write failing tests for POST /admin/themes/:rkey/reset-to-preset (ATB-59)
* test(web): strengthen reset-to-preset 400 assertion (ATB-59)
* feat(web): POST /admin/themes/:rkey/reset-to-preset (ATB-59)
* fix(web): address code review issues — ATB-59
- Fix GET /admin/themes/:rkey to call public /api/themes/:rkey instead
of nonexistent /api/admin/themes/:rkey; remove unused cookie variable
- Validate name before AppView PUT in save handler; redirect with error
if empty (prevents wasteful round-trip and unclear AppView message)
- Replace c.json() with redirect-on-error in reset-to-preset handler so
browser form POSTs show friendly error pages instead of raw JSON
- Add network failure test for GET /admin/themes/:rkey (500 unavailable)
- Add empty-name validation test for save handler
- Move ATB-59 plan docs to docs/plans/complete/
* docs: move ATB-59 plan docs to complete/
Break the 675-line monolithic helpers file into three focused modules:
- helpers/serialize.ts — serialization functions and DB row type aliases
- helpers/validate.ts — input validation and parameter parsing
- helpers/queries.ts — database query helpers (bans, mod status, etc.)
helpers.ts becomes a barrel re-export, so zero consumer changes needed.
This reduces merge conflicts since team members working on admin routes
won't collide with changes to serialization or query helpers.
Also fix admin modlog route: replace drizzle-orm aliased self-joins
(which generate invalid SQL for SQLite) with a batch handle lookup.
This fixes 9 pre-existing test failures in the modlog endpoint.
https://claude.ai/code/session_0119eQacx3ejToSd9c6QEc98
Co-authored-by: Claude <noreply@anthropic.com>
* 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
* docs: add design doc for ATB-57 theme write API endpoints
* docs: add implementation plan for ATB-57 theme write API endpoints
* feat(appview): add manageThemes permission to Admin role (ATB-57)
* feat(appview): POST /api/admin/themes — create theme on Forum PDS (ATB-57)
* test(appview): add 401/403/PDS-failure tests for POST /api/admin/themes (ATB-57)
* feat(appview): PUT /api/admin/themes/:rkey — update theme on Forum PDS (ATB-57)
* feat(appview): DELETE /api/admin/themes/:rkey — delete theme, 409 if default (ATB-57)
* test(appview): assert 409 error body in dark-theme default check (ATB-57)
* test(appview): verify deleteRecord called with exact theme args (ATB-57)
* feat(appview): PUT /api/admin/theme-policy — upsert policy singleton on Forum PDS (ATB-57)
* test(appview): add theme-policy update path and updatedAt assertions (ATB-57)
* refactor(appview): strengthen theme-policy type guards and test assertions (ATB-57)
* docs(bruno): add Admin Themes collection for ATB-57 write endpoints
* docs: mark ATB-57 plan docs complete, move to docs/plans/complete/
* fix(appview): add 503 ForumAgent-not-authenticated tests; fix Bruno error code docs (ATB-57)
* docs: add ATB-55 theme read API design doc
Records approved design for themes table, theme_policies table,
theme_policy_available_themes join table, firehose indexer configs,
and GET /api/themes + GET /api/themes/:rkey + GET /api/theme-policy endpoints.
* docs: add ATB-55 theme API implementation plan
* feat(db): add themes, theme_policies, theme_policy_available_themes tables
Generate Postgres (0013) and SQLite (0001) migrations for the three new
theme tables. Build @atbb/db to verify schema compiles correctly.
* feat(appview): add GET /api/themes, /api/themes/:rkey, /api/theme-policy endpoints (ATB-55)
* feat(appview): index space.atbb.forum.theme and themePolicy from firehose (ATB-55)
* docs(bruno): add Themes API collection (ATB-55)
* docs: ATB-52 CSS token extraction design doc
* docs: ATB-52 implementation plan
* test(web): add failing preset completeness tests (ATB-52)
* test(web): improve preset test descriptions (ATB-52)
* feat(web): add neobrutal-light and neobrutal-dark JSON presets with font-size-xs token (ATB-52)
* feat(web): switch base layout to JSON preset import, remove TS preset (ATB-52)
* fix(web): replace all hardcoded CSS values with design tokens in mod and structure UI (ATB-52)
Define two new AT Proto record types for the theming system:
- space.atbb.forum.theme (tid key) — design tokens, color scheme, CSS overrides, font URLs
- space.atbb.forum.themePolicy (literal:self) — available themes, light/dark defaults, user choice toggle
Uses knownValues for colorScheme extensibility and strongRef wrapped in themeRef named def for CID integrity.
README was missing 3 of 5 packages (atproto, cli, logger).
Deployment guide had SESSION_TTL_DAYS default wrong (30 vs actual 7)
and was missing LOG_LEVEL, SEED_DEFAULT_ROLES, DEFAULT_MEMBER_ROLE
from the optional env vars table. Production env example was missing
FORUM_HANDLE and FORUM_PASSWORD variables.
* feat(web): admin mod action log page — /admin/modlog (ATB-48)
* docs: ATB-48 modlog UI implementation plan and completion notes
* fix(web): wrap modlogRes.json() in try-catch for non-JSON AppView responses (ATB-48)
A proxy returning HTML with HTTP 200 would cause Response.json() to throw
SyntaxError, which isProgrammingError() re-throws, producing an unhandled crash
instead of a 500 error page. Wrap with the same pattern used in the members
and role-assignment handlers.
* docs: ATB-46 mod action log endpoint design
Design for GET /api/admin/modlog — paginated mod action audit log with
double users join for moderator and subject handles, and requireAnyPermission
middleware for OR-based permission checks.
* docs: ATB-46 mod action log implementation plan
Step-by-step TDD plan for GET /api/admin/modlog — requireAnyPermission
middleware, Drizzle alias double join, route handler, and Bruno collection.
* feat(appview): add requireAnyPermission middleware (ATB-46)
* test(appview): failing tests for GET /api/admin/modlog (ATB-46)
* feat(appview): GET /api/admin/modlog with double users leftJoin (ATB-46)
* docs(bruno): GET /api/admin/modlog collection (ATB-46)
* docs: move ATB-46 plan docs to complete/
* fix(appview): scope modlog queries to forumDid (ATB-46)