feat: add title field to topics (#51)
* feat: add title field to topics
Topics (thread-starter posts) now have a dedicated title field separate
from the post body text. This adds the field across the full stack:
- Lexicon: optional `title` string on space.atbb.post (max 120 graphemes)
- Database: nullable `title` column on posts table with migration
- Indexer: stores title from incoming post records
- API: POST /api/topics validates and requires title for new topics
- Web UI: title input on new-topic form, title display in topic list and
thread view (falls back to text slice for older titleless posts)
- Tests: title validation, serialization, and form submission tests
- Bruno: updated API collection docs
https://claude.ai/code/session_01AFY6D5413QU48JULXnSQ5Z
* fix(review): address PR #51 review feedback on topic title feature
- Add validateTopicTitle unit tests mirroring validatePostText suite:
boundary at 120 graphemes, emoji grapheme counting, trim behavior,
and non-string input rejection (null/undefined/number/object)
- Add GET /api/topics/:id round-trip test asserting data.post.title
- Add backward-compat test for null title (pre-migration rows)
- Add title field to serializePost JSDoc response shape
- Add minGraphemes: 1 to post.yaml to close lexicon/AppView gap
- Fix Bruno Create Topic.bru: 400 error list now includes missing title;
constraint description changed to "max 120 graphemes; required"
- Add title: null to Get Topic.bru reply example
- Remove misleading maxlength={1000} from title input (server validates graphemes)
- Change || to ?? for null title fallback in boards.tsx TopicRow
Tracks ATB-35 (strip title from reply records at index time)
* fix(review): address PR #51 second round review feedback
- Fix || → ?? for null title fallback in topics.tsx (web)
- Split combined DB+PDS try block into two separate blocks so a
database error (which may surface as "fetch failed" via postgres.js)
cannot be misclassified as a PDS failure and return the wrong message
- Add comment explaining why title is enforced as required in AppView
despite being optional in the lexicon (AT Protocol schemas cannot
express per-use-case requirements)
- Update 503 database error test to mock getForumByUri instead of
putRecord, accurately targeting the DB lookup phase
- File ATB-36 to track stripping title from reply records at index time
* fix(review): extract ban check to middleware and split DB lookup try blocks
- Add requireNotBanned middleware to permissions.ts so banned users see
"You are banned" before requirePermission can return "Insufficient
permissions" — middleware ordering now encodes the correct UX priority
- Split getForumByUri and getBoardByUri into separate try blocks in
topics.ts so operators can distinguish forum vs board lookup failures
in production logs
- Update vi.mock in topics.test.ts and posts.test.ts to use importOriginal
so requireNotBanned executes its real implementation in ban enforcement
tests while requirePermission remains a pass-through
- Update ban error test operation strings from route-scoped labels to
"requireNotBanned" to match the new middleware location
---------
Co-authored-by: Claude <noreply@anthropic.com>