feat(web): topic view thread display with replies (ATB-29) (#46)
* docs: topic view design for ATB-29
Architecture, components, error handling, and test plan for the
topic thread display page with HTMX Load More and bookmarkable URLs.
* docs: topic view implementation plan for ATB-29
10-task TDD plan: interfaces, error handling, OP rendering, breadcrumb,
replies, locked state, auth gate, pagination, HTMX partial, bookmark support.
* fix(appview): scope forum lookup to forumDid in createMembershipForUser
The forum query used only rkey="self" as a filter, which returns the
wrong row when multiple forums exist in the same database (e.g., real
forum data from CLI init runs alongside test forum rows). Add
eq(forums.did, ctx.config.forumDid) to ensure we always look up the
configured forum, preventing forumUri mismatches in duplicate checks
and bootstrap upgrades.
Also simplify the "throws when forum metadata not found" test to remove
manual DELETE statements that were attempting to delete a real forum
with FK dependencies (categories_forum_id_forums_id_fk), causing the
test to fail. With the DID-scoped query, emptyDb:true + cleanDatabase()
is sufficient to put the DB in the expected state.
Fixes 4 failing tests in membership.test.ts.
* feat(web): topic view type interfaces and component stubs (ATB-29)
* fix(web): restore stub body and remove eslint-disable (ATB-29)
* feat(web): topic view thread display with replies (ATB-29)
- GET /topics/:id renders OP + replies with post number badges (#1, #2, …)
- Breadcrumb: Home → Category → Board → Topic (degrades gracefully on fetch failure)
- Locked topic banner + disabled reply slot
- HTMX Load More with hx-push-url for bookmarkable URLs
- ?offset=N bookmark support renders all replies 0→N+pageSize inline
- Auth-gated reply form slot placeholder (unauthenticated / authenticated / locked)
- Full error handling: 400 (invalid ID), 404 (not found), 503 (network), 500 (server)
- TypeError/programming errors re-thrown to global error handler
- 35 tests covering all acceptance criteria
- Remove outdated topics stub tests now superseded by comprehensive test suite
* docs: mark ATB-29 topic view complete in plan
Update phase 4 checklist with implementation notes covering
three-stage fetch, HTMX Load More, breadcrumb degradation,
locked topic handling, and 35 integration tests.
* fix(review): address PR #46 code review feedback
Critical:
- Wrap res.json() in try-catch in fetchApi to prevent SyntaxError from
malformed AppView responses propagating as a programming error (would
have caused isProgrammingError() to re-throw, defeating non-fatal
behavior of breadcrumb fetch stages 2+3)
Important:
- Add multi-tenant isolation regression test in membership.test.ts:
verifies forum lookup is scoped to ctx.config.forumDid by inserting a
forum with a different DID and asserting 'Forum not found' is thrown
- Fix misleading comment in topics test: clarify that stage 1 returns
null (not a fetch error), and stage 2 crashes on null.post.boardId
- Remove two unused mock calls in topics re-throws TypeError test
- Add TODO(ATB-33) comment on client-side reply slicing in topics.tsx
Tests added:
- api.test.ts: malformed JSON from AppView throws response error
- topics.test.tsx: locked + authenticated shows disabled message
- topics.test.tsx: null boardId skips stages 2 and 3 entirely
- topics.test.tsx: HTMX partial re-throws TypeError (programming error)
- topics.test.tsx: breadcrumb links use correct /categories/:id URLs
Bug fix:
- Category breadcrumb was linking to '/' instead of '/categories/:id'
* fix(test): eliminate cleanDatabase() race condition in isolation test
The isolation test previously called createTestContext({ emptyDb: true })
inside the test body, which triggered cleanDatabase() and deleted the
did:plc:test-forum row that concurrently-running forum.test.ts depended
on — causing a flaky "description is object (null)" failure in CI.
Fix: spread ctx with an overridden forumDid instead of creating a new
context. This keeps the same DB connection and never calls cleanDatabase(),
eliminating the race. The test still proves the forumDid scoping: with
did:plc:test-forum in the DB and isolationCtx.forumDid pointing to a
non-existent DID, a broken implementation would find the wrong forum
instead of throwing "Forum not found".
Also removes unused forums import added in previous commit.