fix(webapp): opening a thread marks it read by an explicit POST; a test pins which GETs may write (#1163) #1177

Merged
claude-bot merged 3 commits from fix/1163-get-routes-that-write into main 2026-10-13 18:46:04 +00:00
Collaborator

Closes #1163 (the logout part merged in #1165).

Threads. GET /discussions/{id} no longer marks the thread read. New POST /discussions/{id}/read (204, idempotent, same auth and 404 as the GET). The thread page calls it when the thread is shown: once on open, again after a refetch only if that refetch brought a new post into view, and never while the tab is hidden (it marks when the tab becomes visible). A failed mark-read is swallowed and never breaks the page. From the reader's side nothing changes: opening a thread clears its unread state, and so does a post that arrives while they are looking at it.

The test. tests/test_get_routes_do_not_write.py walks every GET route, scans the handler and one level of app.* callees for writes (commit/add/delete, update/insert/delete statements, Redis writes, enqueue), and fails on any writer not on an allow-list of (module, function, reason). Its docstring states what it cannot see (a write behind a second helper, or through an unusually named session). A stale allow-list entry also fails.

Allow-listed, read and confirmed: the OIDC callback and Discord link GETs; the lore image GET (lazy move of a legacy file); the campaign ownership offer GET (lazy expiry); the four sheet-definition GETs (ensure_builtin).

Allow-listed, found by the scan and not yet reviewed (follow-up, not this PR): the planning names GET (writes a generation_results row on every cached call, and runs the LLM synchronously on a cold cache; wants to be a POST); GET /admin/settings/secrets-health (upgrades plaintext secrets and commits); the two content-pack export GETs (an audit row each); the server-ownership offer GET (lazy expiry).

No bot API change (/api/bot/* has no thread GET), no migration.

Verified

On docker-test: tests/discussions, the new test, test_wiki_crawl.py, test_commit_before_response.py, tests/export/test_export_bin.py (113 passed); ruff; changelog and version checks; discussions vitest (57 passed); frontend lint. Journeys run for real in the desktop project: J6 (opens a thread) on the final page code, and J2 (creates a thread) before the last change.

Not run: phone and keyboard projects, the full backend and frontend suites (CI). This PR touches no journey or stack file, so it should take the desktop-only path from #1174: the first real run of it.

🤖 Generated with Claude Code

Closes #1163 (the logout part merged in #1165). **Threads.** `GET /discussions/{id}` no longer marks the thread read. New `POST /discussions/{id}/read` (204, idempotent, same auth and 404 as the GET). The thread page calls it when the thread is shown: once on open, again after a refetch only if that refetch brought a new post into view, and never while the tab is hidden (it marks when the tab becomes visible). A failed mark-read is swallowed and never breaks the page. From the reader's side nothing changes: opening a thread clears its unread state, and so does a post that arrives while they are looking at it. **The test.** `tests/test_get_routes_do_not_write.py` walks every GET route, scans the handler and one level of `app.*` callees for writes (commit/add/delete, update/insert/delete statements, Redis writes, `enqueue`), and fails on any writer not on an allow-list of `(module, function, reason)`. Its docstring states what it cannot see (a write behind a second helper, or through an unusually named session). A stale allow-list entry also fails. **Allow-listed, read and confirmed:** the OIDC callback and Discord link GETs; the lore image GET (lazy move of a legacy file); the campaign ownership offer GET (lazy expiry); the four sheet-definition GETs (`ensure_builtin`). **Allow-listed, found by the scan and not yet reviewed** (follow-up, not this PR): the planning names GET (writes a `generation_results` row on every cached call, and runs the LLM synchronously on a cold cache; wants to be a POST); `GET /admin/settings/secrets-health` (upgrades plaintext secrets and commits); the two content-pack export GETs (an audit row each); the server-ownership offer GET (lazy expiry). No bot API change (`/api/bot/*` has no thread GET), no migration. ## Verified On `docker-test`: `tests/discussions`, the new test, `test_wiki_crawl.py`, `test_commit_before_response.py`, `tests/export/test_export_bin.py` (113 passed); ruff; changelog and version checks; discussions vitest (57 passed); frontend lint. Journeys run for real in the desktop project: J6 (opens a thread) on the final page code, and J2 (creates a thread) before the last change. Not run: phone and keyboard projects, the full backend and frontend suites (CI). This PR touches no journey or stack file, so it should take the desktop-only path from #1174: the first real run of it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
POST /api/discussions/{id}/read (204, idempotent, same 404 as the GET); the
thread page calls it once when it shows the thread. A static scan test lists
the GET handlers allowed to write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test: hidden-tab case (#1163)
All checks were successful
CI / Does this change touch the recording path? (pull_request) Successful in 25s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Docker image build (pull_request) Successful in 42s
CI / Which journeys does this change need? (pull_request) Successful in 51s
CI / Bot/backend version sync (pull_request) Successful in 1m1s
CI / Backend lint (ruff) (pull_request) Successful in 1m3s
CI / Feedback site tests (pull_request) Successful in 1m23s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m37s
CI / Bot tests and audit (pull_request) Successful in 2m45s
CI / Frontend (shard 2/2) (pull_request) Successful in 8m1s
CI / Frontend (shard 1/2) (pull_request) Successful in 10m3s
CI / End-to-end journeys (desktop) (desktop) (pull_request) Successful in 14m33s
CI / Backend migration, tests, and audit (pull_request) Successful in 42m41s
0edd4707fc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-10-13 18:03:22 +00:00
claude-bot deleted branch fix/1163-get-routes-that-write 2026-10-13 18:46:06 +00:00
Sign in to join this conversation.
No description provided.