commits
Swatch spans were invisible because <span> collapses to zero size without
explicit dimensions. Also adds layout styles for settings-page, banners,
and form that were never written.
- Add syncRepoRecords dispatch tests for space.atbb.forum.theme and
space.atbb.forum.themePolicy — proves handleThemeCreate and
handleThemePolicyCreate are actually invoked, catches renames silently
- Add test verifying TypeError propagates when handler method is absent
on Indexer (covers the as-any cast gap)
- Re-throw isProgrammingError in syncRepoRecords outer catch so handler
bugs are not silently logged as pds_error
- Add null guard in themePolicyConfig.toInsertValues / toUpdateValues for
missing defaultLightTheme/defaultDarkTheme refs; returns null to skip
the insert rather than crashing with TypeError on malformed records
Themes and theme policies were handled by the firehose but omitted from
FORUM_OWNED_COLLECTIONS and COLLECTION_HANDLER_MAP, so a backfill after
restart would never replay them from the PDS — causing 404s when the web
app tried to resolve a theme URI that existed on the PDS but not in the DB.
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
* docs: add user theme preferences design plan
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
* feat: add resolveUserThemePreference to theme resolution waterfall
* test: resolveUserThemePreference unit and resolveTheme integration tests
* fix: use Array<T> syntax instead of T[] for complex object types
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()
* feat: add settings page with light/dark theme preference form
* test: settings route GET and POST integration tests
* fix: address code review feedback on Phase 2 settings routes
- [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.
* fix: use Array<string> instead of string[] in settings test helper
* feat: add HTMX theme preview endpoint and wire up select elements
* test: settings preview endpoint and HTMX attribute tests
* feat: add Settings nav link for authenticated users
* test: Settings nav link auth visibility tests
* docs: add Bruno collection entries for settings endpoints
* docs: update project context for user-theme-preferences branch
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.
* docs: clarify preview endpoint is unauthenticated in CLAUDE.md
* docs: add test plan for user theme preferences
* chore: gitignore .claude/ and add user theme preferences implementation plan
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.
* fix: address PR review feedback on user theme preferences
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>
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)
* feat(appview): add uri field to serializeCategory (ATB-47)
* test(web): add failing tests for GET /admin/structure (ATB-47)
* feat(web): add GET /admin/structure page with category/board listing (ATB-47)
* fix(web): use lowercase method="post" on structure page forms (ATB-47)
* test(web): add failing tests for category proxy routes (ATB-47)
* feat(web): add category proxy routes for structure management (ATB-47)
* test(web): add failing tests for board proxy routes (ATB-47)
* feat(web): add board proxy routes for structure management (ATB-47)
* feat(web): add CSS for admin structure management page (ATB-47)
* test(web): add missing network error tests for edit proxy routes (ATB-47)
* fix(web): log errors when boards fetch fails per-category in structure page (ATB-47)
* docs: add completed implementation plan for ATB-47 admin structure UI
* fix(web): validate sort order and fix board delete 409 test (ATB-47)
- parseSortOrder now returns null for negative/non-integer values and
redirects with "Sort order must be a non-negative integer." error;
0 remains the default for empty/missing sort order fields
- Use Number() + Number.isInteger() instead of parseInt() to reject
floats like "1.5" that parseInt would silently truncate
- Add validation tests for negative sort order across all 4 create/edit
handlers (create category, edit category, create board, edit board)
- Fix board delete 409 test mock to use the real AppView error message
("Cannot delete board with posts. Remove all posts first.") and assert
the message appears in the redirect URL, matching category delete test
* test(appview): add failing tests for POST /api/admin/boards (ATB-45)
* test(appview): add ForumAgent not authenticated test for POST /api/admin/boards (ATB-45)
* feat(appview): POST /api/admin/boards create endpoint (ATB-45)
* test(appview): add failing tests for PUT /api/admin/boards/:id (ATB-45)
* test(appview): add error body assertion to PUT boards malformed JSON test (ATB-45)
* feat(appview): PUT /api/admin/boards/:id update endpoint (ATB-45)
* test(appview): add failing tests for DELETE /api/admin/boards/:id (ATB-45)
* test(appview): improve DELETE /api/admin/boards/:id test coverage (ATB-45)
* feat(appview): DELETE /api/admin/boards/:id delete endpoint (ATB-45)
Pre-flight refuses with 409 if any posts reference the board (via posts.boardId).
Also fixes test error messages to use "Database connection lost" (matching isDatabaseError keywords) for consistent 503 classification.
* docs(bruno): add board management API collection (ATB-45)
* fix(appview): close postgres connection after each admin test to prevent pool exhaustion (ATB-45)
Each createTestContext() call opens a new postgres.js connection pool. With 93
tests in admin.test.ts, the old pools were never closed, exhausting PostgreSQL's
max_connections limit. Fix by calling $client.end() in cleanup() for Postgres.
* docs(plans): move ATB-44 and ATB-45 plan docs to complete/
* test(appview): add missing DB error 503 tests for board endpoints (ATB-45)
- POST /api/admin/boards: add "returns 503 when category lookup query fails"
- PUT /api/admin/boards/:id: add "returns 503 when board lookup query fails"
- PUT /api/admin/boards/:id: add "returns 503 when category CID lookup query fails"
(call-count pattern: first select passes, second throws)
Replace loose pattern checks (toMatch, toContain) with an exact toBe()
assertion using the seeded role's full AT URI. Also assert toHaveLength(1)
so the test fails if extra roles appear unexpectedly.
Add a startsWith("did:") guard in the POST /admin/members/:did/role handler
before the upstream fetch call. Malformed path parameters now return an inline
MemberRow error fragment without hitting the AppView. Covered by a new test.
* feat(appview): POST /api/admin/categories create endpoint (ATB-44)
* feat(appview): PUT /api/admin/categories/:id update endpoint (ATB-44)
* test(appview): add malformed JSON test for PUT /api/admin/categories/:id (ATB-44)
* test(appview): add failing tests for DELETE /api/admin/categories/:id (ATB-44)
* feat(appview): DELETE /api/admin/categories/:id delete endpoint (ATB-44)
* docs(bruno): add category management API collection (ATB-44)
* fix(appview): use handleRouteError after consolidation refactor (ATB-44)
PR #74 consolidated handleReadError, handleWriteError, and
handleSecurityCheckError into a single handleRouteError. Update the
new category management handlers added in this branch to use the
consolidated name.
* fix(appview): address category endpoint review feedback (ATB-44)
- Tighten sortOrder validation: Number.isInteger() && >= 0 instead of
typeof === "number" (rejects floats, negatives, NaN, Infinity per lexicon
constraint integer, minimum: 0)
- Add 503 "ForumAgent not authenticated" tests for POST, PUT, DELETE
- Add 503 database failure tests for PUT and DELETE category lookup
- Add 403 permission tests for POST, PUT, DELETE
* fix(appview): address final review feedback on category endpoints (ATB-44)
- Fix PUT data loss: putRecord is a full AT Protocol record replacement,
not a patch. Fall back to existing category.description and
category.sortOrder when not provided in request body.
- Add test verifying existing description/sortOrder are preserved on
partial updates (regression test for the data loss bug).
- Add test for DELETE board-count preflight query failure path (503),
using a call-count mock so category lookup succeeds while the second
select throws.
* fix(web): show reply count and last-reply date on board topic listing
The topic listing on board pages always showed "0 replies" (hardcoded)
and used the topic's own createdAt for the date instead of the most
recent reply's timestamp.
- Add getReplyStats() helper: single GROUP BY query computing COUNT() and
MAX(createdAt) per rootPostId for a batch of topic IDs in one round-trip
- Enrich GET /api/boards/:id/topics response with replyCount and lastReplyAt
per topic; fail-open so a stats query failure degrades gracefully to 0/null
- Update TopicResponse interface and TopicRow in boards.tsx to consume the
new fields; date now reflects lastReplyAt ?? createdAt
- Add 5 integration tests covering zero replies, non-banned count, MAX date
accuracy, banned-reply exclusion, and per-topic independence
* fix(web): address code review issues from PR #75
- Add fail-open error path test: mocks getReplyStats DB failure and
asserts 200 response with replyCount 0 / lastReplyAt null / logger.error called
- Fix UI attribution: when lastReplyAt is set, show "last reply X ago"
instead of raw date next to "by {author}" — disambiguates whose action
the timestamp refers to
- Update Bruno collection: add replyCount and lastReplyAt to docs example
and assert blocks for GET /api/boards/:id/topics
- Rebase: merge conflict in boards.ts imports resolved (handleReadError →
handleRouteError from PR #74 refactor)
* fix(web): update setupSuccessfulFetch type to include replyCount and lastReplyAt
The helper's topics array element type was not updated alongside makeTopicsResponse,
causing tsc to reject the new test cases with TS2353. Vitest strips types via esbuild
so it passed locally; tsc in CI caught the mismatch.
handleReadError, handleWriteError, and handleSecurityCheckError had
byte-for-byte identical implementations. Replaced with a single
handleRouteError function, eliminating ~250 lines of duplicated code
across the error handler and its tests. Updated all 10 call sites.
https://claude.ai/code/session_018SH9pay3PqGo9JDdAv3iRj
Co-authored-by: Claude <noreply@anthropic.com>
* feat(web): add canManageRoles session helper (ATB-43)
* feat(appview): include uri in GET /api/admin/roles response (ATB-43)
Add rkey and did fields to the roles DB query, then construct the AT URI
(at://<did>/space.atbb.forum.role/<rkey>) in the response map so the
admin members page dropdown can submit a valid roleUri.
* style(web): add admin member table CSS classes (ATB-43)
* feat(web): add GET /admin/members page and POST proxy route (ATB-43)
* fix(web): add manageRoles permission gate to POST proxy route (ATB-43)
* docs: mark ATB-42 and ATB-43 complete in project plan
* docs: add ATB-43 implementation plan
* fix(web): address PR review feedback on admin members page (ATB-43)
* fix(web): use canManageRoles(auth) instead of hardcoded false in rolesJson error path
* docs: ATB-42 admin panel landing page implementation plan
* feat(web): add hasAnyAdminPermission() helper to session.ts (ATB-42)
* test(web): add hasAnyAdminPermission tests + tighten JSDoc (ATB-42)
* feat(web): add GET /admin landing page with permission-gated nav cards (ATB-42)
* refactor(web): move canManageMembers/canManageCategories/canViewModLog to session.ts (ATB-42)
* test(web): add admin landing page route tests (ATB-42)
* test(web): add missing structure-absent assertions for banUsers/lockTopics (ATB-42)
* style(web): add admin nav grid CSS (ATB-42)
* docs: add admin panel UI preview screenshot (ATB-42)
* fix(web): address minor code review feedback on ATB-42 admin panel
- Use var(--font-size-xl, 2rem) for admin card icon (CSS token consistency)
- Add banUsers and lockTopics test cases for canViewModLog helper
- Move plan doc to docs/plans/complete/
Step-by-step plan: add axe-core + jsdom deps, create consolidated test
file with jsdom env pragma, one happy-path WCAG AA test per page route.
Captures the approved design for adding axe-core + jsdom automated
accessibility tests to apps/web — single consolidated test file,
per-file jsdom environment pragma, one happy-path test per page route.
* docs: ATB-34 axe-core a11y testing design
Captures the approved design for adding axe-core + jsdom automated
accessibility tests to apps/web — single consolidated test file,
per-file jsdom environment pragma, one happy-path test per page route.
* docs: ATB-34 axe-core a11y implementation plan
Step-by-step plan: add axe-core + jsdom deps, create consolidated test
file with jsdom env pragma, one happy-path WCAG AA test per page route.
* chore(web): add axe-core, jsdom, vitest as explicit devDependencies (ATB-34)
* test(web): scaffold a11y test file with jsdom environment and module mocks (ATB-34)
* test(web): add WCAG AA accessibility tests for all page routes (ATB-34)
* test(web): suppress document.write deprecation with explanatory comment (ATB-34)
* test(web): use @ts-ignore to suppress deprecated document.write diagnostic (ATB-34)
* docs: move ATB-34 plan docs to complete
* test(web): address PR review feedback on a11y tests (ATB-34)
- Fix DOMParser comment to explain axe isPageContext() mechanism accurately
- Add DOM replacement guard after document.write() to catch silent no-ops
- Wrap axe.run() in try/catch with routeLabel for infrastructure error context
- Add routeLabel param to checkA11y; update all 6 call sites
- Reset canLockTopics/canModeratePosts/canBanUsers in beforeEach
- Add afterEach DOM cleanup via documentElement.innerHTML
- Fix path.startsWith('/topics/1') to exact match '/topics/1?offset=0&limit=25'
- Add form presence guard in new-topic test to catch silent auth fallback
- Update design doc to document DOMParser divergence and its reason
* test(web): strengthen DOM write guard to check html[lang] (ATB-34)
afterEach now removes lang from <html> so the guard in checkA11y can use
html[lang] as proof that document.write() actually executed, rather than
just checking that the <html> element exists (which it always does after
afterEach resets innerHTML without touching attributes).
Adds missing \$type fields to forum and board ref objects written to the PDS,
consistent with the replyRef fix in PR #61. Without \$type, the AT Protocol
runtime guards Post.isForumRef()/isBoardRef() return false, which would silently
break any future indexer refactor that adopts typed guards over optional chaining.
Also adds Post.isForumRef()/isBoardRef() assertions to the corresponding tests,
following the same contract-based pattern established for replyRef.
Swatch spans were invisible because <span> collapses to zero size without
explicit dimensions. Also adds layout styles for settings-page, banners,
and form that were never written.
- Add syncRepoRecords dispatch tests for space.atbb.forum.theme and
space.atbb.forum.themePolicy — proves handleThemeCreate and
handleThemePolicyCreate are actually invoked, catches renames silently
- Add test verifying TypeError propagates when handler method is absent
on Indexer (covers the as-any cast gap)
- Re-throw isProgrammingError in syncRepoRecords outer catch so handler
bugs are not silently logged as pds_error
- Add null guard in themePolicyConfig.toInsertValues / toUpdateValues for
missing defaultLightTheme/defaultDarkTheme refs; returns null to skip
the insert rather than crashing with TypeError on malformed records
* docs: add user theme preferences design plan
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
* feat: add resolveUserThemePreference to theme resolution waterfall
* test: resolveUserThemePreference unit and resolveTheme integration tests
* fix: use Array<T> syntax instead of T[] for complex object types
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()
* feat: add settings page with light/dark theme preference form
* test: settings route GET and POST integration tests
* fix: address code review feedback on Phase 2 settings routes
- [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.
* fix: use Array<string> instead of string[] in settings test helper
* feat: add HTMX theme preview endpoint and wire up select elements
* test: settings preview endpoint and HTMX attribute tests
* feat: add Settings nav link for authenticated users
* test: Settings nav link auth visibility tests
* docs: add Bruno collection entries for settings endpoints
* docs: update project context for user-theme-preferences branch
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.
* docs: clarify preview endpoint is unauthenticated in CLAUDE.md
* docs: add test plan for user theme preferences
* chore: gitignore .claude/ and add user theme preferences implementation plan
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.
* fix: address PR review feedback on user theme preferences
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)
* feat(appview): add uri field to serializeCategory (ATB-47)
* test(web): add failing tests for GET /admin/structure (ATB-47)
* feat(web): add GET /admin/structure page with category/board listing (ATB-47)
* fix(web): use lowercase method="post" on structure page forms (ATB-47)
* test(web): add failing tests for category proxy routes (ATB-47)
* feat(web): add category proxy routes for structure management (ATB-47)
* test(web): add failing tests for board proxy routes (ATB-47)
* feat(web): add board proxy routes for structure management (ATB-47)
* feat(web): add CSS for admin structure management page (ATB-47)
* test(web): add missing network error tests for edit proxy routes (ATB-47)
* fix(web): log errors when boards fetch fails per-category in structure page (ATB-47)
* docs: add completed implementation plan for ATB-47 admin structure UI
* fix(web): validate sort order and fix board delete 409 test (ATB-47)
- parseSortOrder now returns null for negative/non-integer values and
redirects with "Sort order must be a non-negative integer." error;
0 remains the default for empty/missing sort order fields
- Use Number() + Number.isInteger() instead of parseInt() to reject
floats like "1.5" that parseInt would silently truncate
- Add validation tests for negative sort order across all 4 create/edit
handlers (create category, edit category, create board, edit board)
- Fix board delete 409 test mock to use the real AppView error message
("Cannot delete board with posts. Remove all posts first.") and assert
the message appears in the redirect URL, matching category delete test
* test(appview): add failing tests for POST /api/admin/boards (ATB-45)
* test(appview): add ForumAgent not authenticated test for POST /api/admin/boards (ATB-45)
* feat(appview): POST /api/admin/boards create endpoint (ATB-45)
* test(appview): add failing tests for PUT /api/admin/boards/:id (ATB-45)
* test(appview): add error body assertion to PUT boards malformed JSON test (ATB-45)
* feat(appview): PUT /api/admin/boards/:id update endpoint (ATB-45)
* test(appview): add failing tests for DELETE /api/admin/boards/:id (ATB-45)
* test(appview): improve DELETE /api/admin/boards/:id test coverage (ATB-45)
* feat(appview): DELETE /api/admin/boards/:id delete endpoint (ATB-45)
Pre-flight refuses with 409 if any posts reference the board (via posts.boardId).
Also fixes test error messages to use "Database connection lost" (matching isDatabaseError keywords) for consistent 503 classification.
* docs(bruno): add board management API collection (ATB-45)
* fix(appview): close postgres connection after each admin test to prevent pool exhaustion (ATB-45)
Each createTestContext() call opens a new postgres.js connection pool. With 93
tests in admin.test.ts, the old pools were never closed, exhausting PostgreSQL's
max_connections limit. Fix by calling $client.end() in cleanup() for Postgres.
* docs(plans): move ATB-44 and ATB-45 plan docs to complete/
* test(appview): add missing DB error 503 tests for board endpoints (ATB-45)
- POST /api/admin/boards: add "returns 503 when category lookup query fails"
- PUT /api/admin/boards/:id: add "returns 503 when board lookup query fails"
- PUT /api/admin/boards/:id: add "returns 503 when category CID lookup query fails"
(call-count pattern: first select passes, second throws)
* feat(appview): POST /api/admin/categories create endpoint (ATB-44)
* feat(appview): PUT /api/admin/categories/:id update endpoint (ATB-44)
* test(appview): add malformed JSON test for PUT /api/admin/categories/:id (ATB-44)
* test(appview): add failing tests for DELETE /api/admin/categories/:id (ATB-44)
* feat(appview): DELETE /api/admin/categories/:id delete endpoint (ATB-44)
* docs(bruno): add category management API collection (ATB-44)
* fix(appview): use handleRouteError after consolidation refactor (ATB-44)
PR #74 consolidated handleReadError, handleWriteError, and
handleSecurityCheckError into a single handleRouteError. Update the
new category management handlers added in this branch to use the
consolidated name.
* fix(appview): address category endpoint review feedback (ATB-44)
- Tighten sortOrder validation: Number.isInteger() && >= 0 instead of
typeof === "number" (rejects floats, negatives, NaN, Infinity per lexicon
constraint integer, minimum: 0)
- Add 503 "ForumAgent not authenticated" tests for POST, PUT, DELETE
- Add 503 database failure tests for PUT and DELETE category lookup
- Add 403 permission tests for POST, PUT, DELETE
* fix(appview): address final review feedback on category endpoints (ATB-44)
- Fix PUT data loss: putRecord is a full AT Protocol record replacement,
not a patch. Fall back to existing category.description and
category.sortOrder when not provided in request body.
- Add test verifying existing description/sortOrder are preserved on
partial updates (regression test for the data loss bug).
- Add test for DELETE board-count preflight query failure path (503),
using a call-count mock so category lookup succeeds while the second
select throws.
* fix(web): show reply count and last-reply date on board topic listing
The topic listing on board pages always showed "0 replies" (hardcoded)
and used the topic's own createdAt for the date instead of the most
recent reply's timestamp.
- Add getReplyStats() helper: single GROUP BY query computing COUNT() and
MAX(createdAt) per rootPostId for a batch of topic IDs in one round-trip
- Enrich GET /api/boards/:id/topics response with replyCount and lastReplyAt
per topic; fail-open so a stats query failure degrades gracefully to 0/null
- Update TopicResponse interface and TopicRow in boards.tsx to consume the
new fields; date now reflects lastReplyAt ?? createdAt
- Add 5 integration tests covering zero replies, non-banned count, MAX date
accuracy, banned-reply exclusion, and per-topic independence
* fix(web): address code review issues from PR #75
- Add fail-open error path test: mocks getReplyStats DB failure and
asserts 200 response with replyCount 0 / lastReplyAt null / logger.error called
- Fix UI attribution: when lastReplyAt is set, show "last reply X ago"
instead of raw date next to "by {author}" — disambiguates whose action
the timestamp refers to
- Update Bruno collection: add replyCount and lastReplyAt to docs example
and assert blocks for GET /api/boards/:id/topics
- Rebase: merge conflict in boards.ts imports resolved (handleReadError →
handleRouteError from PR #74 refactor)
* fix(web): update setupSuccessfulFetch type to include replyCount and lastReplyAt
The helper's topics array element type was not updated alongside makeTopicsResponse,
causing tsc to reject the new test cases with TS2353. Vitest strips types via esbuild
so it passed locally; tsc in CI caught the mismatch.
handleReadError, handleWriteError, and handleSecurityCheckError had
byte-for-byte identical implementations. Replaced with a single
handleRouteError function, eliminating ~250 lines of duplicated code
across the error handler and its tests. Updated all 10 call sites.
https://claude.ai/code/session_018SH9pay3PqGo9JDdAv3iRj
Co-authored-by: Claude <noreply@anthropic.com>
* feat(web): add canManageRoles session helper (ATB-43)
* feat(appview): include uri in GET /api/admin/roles response (ATB-43)
Add rkey and did fields to the roles DB query, then construct the AT URI
(at://<did>/space.atbb.forum.role/<rkey>) in the response map so the
admin members page dropdown can submit a valid roleUri.
* style(web): add admin member table CSS classes (ATB-43)
* feat(web): add GET /admin/members page and POST proxy route (ATB-43)
* fix(web): add manageRoles permission gate to POST proxy route (ATB-43)
* docs: mark ATB-42 and ATB-43 complete in project plan
* docs: add ATB-43 implementation plan
* fix(web): address PR review feedback on admin members page (ATB-43)
* fix(web): use canManageRoles(auth) instead of hardcoded false in rolesJson error path
* docs: ATB-42 admin panel landing page implementation plan
* feat(web): add hasAnyAdminPermission() helper to session.ts (ATB-42)
* test(web): add hasAnyAdminPermission tests + tighten JSDoc (ATB-42)
* feat(web): add GET /admin landing page with permission-gated nav cards (ATB-42)
* refactor(web): move canManageMembers/canManageCategories/canViewModLog to session.ts (ATB-42)
* test(web): add admin landing page route tests (ATB-42)
* test(web): add missing structure-absent assertions for banUsers/lockTopics (ATB-42)
* style(web): add admin nav grid CSS (ATB-42)
* docs: add admin panel UI preview screenshot (ATB-42)
* fix(web): address minor code review feedback on ATB-42 admin panel
- Use var(--font-size-xl, 2rem) for admin card icon (CSS token consistency)
- Add banUsers and lockTopics test cases for canViewModLog helper
- Move plan doc to docs/plans/complete/
* docs: ATB-34 axe-core a11y testing design
Captures the approved design for adding axe-core + jsdom automated
accessibility tests to apps/web — single consolidated test file,
per-file jsdom environment pragma, one happy-path test per page route.
* docs: ATB-34 axe-core a11y implementation plan
Step-by-step plan: add axe-core + jsdom deps, create consolidated test
file with jsdom env pragma, one happy-path WCAG AA test per page route.
* chore(web): add axe-core, jsdom, vitest as explicit devDependencies (ATB-34)
* test(web): scaffold a11y test file with jsdom environment and module mocks (ATB-34)
* test(web): add WCAG AA accessibility tests for all page routes (ATB-34)
* test(web): suppress document.write deprecation with explanatory comment (ATB-34)
* test(web): use @ts-ignore to suppress deprecated document.write diagnostic (ATB-34)
* docs: move ATB-34 plan docs to complete
* test(web): address PR review feedback on a11y tests (ATB-34)
- Fix DOMParser comment to explain axe isPageContext() mechanism accurately
- Add DOM replacement guard after document.write() to catch silent no-ops
- Wrap axe.run() in try/catch with routeLabel for infrastructure error context
- Add routeLabel param to checkA11y; update all 6 call sites
- Reset canLockTopics/canModeratePosts/canBanUsers in beforeEach
- Add afterEach DOM cleanup via documentElement.innerHTML
- Fix path.startsWith('/topics/1') to exact match '/topics/1?offset=0&limit=25'
- Add form presence guard in new-topic test to catch silent auth fallback
- Update design doc to document DOMParser divergence and its reason
* test(web): strengthen DOM write guard to check html[lang] (ATB-34)
afterEach now removes lang from <html> so the guard in checkA11y can use
html[lang] as proof that document.write() actually executed, rather than
just checking that the <html> element exists (which it always does after
afterEach resets innerHTML without touching attributes).
Adds missing \$type fields to forum and board ref objects written to the PDS,
consistent with the replyRef fix in PR #61. Without \$type, the AT Protocol
runtime guards Post.isForumRef()/isBoardRef() return false, which would silently
break any future indexer refactor that adopts typed guards over optional chaining.
Also adds Post.isForumRef()/isBoardRef() assertions to the corresponding tests,
following the same contract-based pattern established for replyRef.