Lane 8 PR 6: the map, server side (#734) #903

Merged
claude-bot merged 3 commits from build/lane8-6-map into main 2026-09-28 05:19:33 +00:00
Collaborator

Part of #734 (lane 8), §3.9. Server only; P7 builds the pages.

Migration 8b8c9d0e1f2a_maps (on 8a7b, lane 8's head on main)

  • New tables: atlases, maps, map_markers, map_links and trail_stops, plus people-audience tables for atlases, maps and markers.
  • No notification CHECK is touched. A map reveal is on screen only, and a test asserts it.
  • Up → down → up passes with seeded rows: an atlas, a map with its image and tiles, a revealed GM-only marker, a link and a trail stop. There's also an index-parity check against the models.
  • The one-head pin moved here, and test_notifications' history is extended.
  • A cross-migration test fix: PR 3's parties downgrade drops the map tables' foreign keys on parties first. That order only happens in the isolated-migration test; the comment follows PR 5's precedent.

What it builds

  • Audiences (map_audience.py) use lane 7's model: table, party, mine, chosen or gm.
    • A marker is readable only inside both its own audience and its map's.
    • A map is readable only inside both its own audience and its atlas's.
  • CRUD for atlases, maps, markers, links and trail stops. Every list filters by audience, and every single item 404s rather than 403s outside it (D167: confirming that something exists is itself a leak).
  • Images have their own 75 MB cap (W9/D195), separate from the 8 MB media cap.
  • Tiling: cut_map_tiles, queued with await enqueue, cuts a 256 px JPEG tile layer and tracks tiles_status (none, cutting, ready or failed).
  • Reveal and undo on markers. Only a GM-audience marker can be revealed, and only by a GM.

Privacy: private images and tiles

  • Every map image and tile is stored under media/private/ and served only through the authorised X-Accel-Redirect route. That's the same mechanism as #893's handouts, so no nginx change was needed.
  • Tests:
    • a non-member, and a member outside a mine or party map's audience, get 404 for both the image and a tile;
    • the named member, and the GM, get through;
    • a tile 404s until it's ready;
    • #893's nginx proof applies: the public /media/private/ returns 404.
  • Backups: the nightly media archive tars all of media_root, private/ included, so map images are backed up. The survey's note that they aren't is out of date.

Departures (for P7)

  1. Tiling is a single zoom level. Lower-resolution levels for a very large map can be added later without a schema change.
  2. IS-076's rule that a player's marker has three choices is a P7 UI restriction. The server keeps one audience picker for every surface.
  3. A marker reveal writes no wiki_events row, because lore_entry_id is NOT NULL and a marker needn't have a page. If reveals should reach Recent changes, that's P7's feed work.
  4. Atlas nesting and navigation is P7's.

Checks

  • Full backend suite: 5785 passed, 0 failed. The only exclusions were the two local-mount artifacts.
  • Targeted modules: 586 passed.
  • ruff: clean.
  • Changelog: a changelog.d/ fragment.

🤖 Generated with Claude Code

Part of #734 (lane 8), §3.9. Server only; P7 builds the pages. ## Migration `8b8c9d0e1f2a_maps` (on `8a7b`, lane 8's head on main) - **New tables:** `atlases`, `maps`, `map_markers`, `map_links` and `trail_stops`, plus people-audience tables for atlases, maps and markers. - **No notification CHECK is touched.** A map reveal is on screen only, and a test asserts it. - **Up → down → up passes with seeded rows:** an atlas, a map with its image and tiles, a revealed GM-only marker, a link and a trail stop. There's also an index-parity check against the models. - The one-head pin moved here, and `test_notifications`' history is extended. - **A cross-migration test fix:** PR 3's parties downgrade drops the map tables' foreign keys on `parties` first. That order only happens in the isolated-migration test; the comment follows PR 5's precedent. ## What it builds - **Audiences** (`map_audience.py`) use lane 7's model: table, party, mine, chosen or gm. - A marker is readable only inside both its own audience and its map's. - A map is readable only inside both its own audience and its atlas's. - **CRUD** for atlases, maps, markers, links and trail stops. Every list filters by audience, and every single item **404s rather than 403s** outside it (D167: confirming that something exists is itself a leak). - **Images** have their own 75 MB cap (W9/D195), separate from the 8 MB media cap. - **Tiling:** `cut_map_tiles`, queued with `await enqueue`, cuts a 256 px JPEG tile layer and tracks `tiles_status` (none, cutting, ready or failed). - **Reveal and undo** on markers. Only a GM-audience marker can be revealed, and only by a GM. ## Privacy: private images and tiles - **Every map image and tile** is stored under `media/private/` and served only through the authorised `X-Accel-Redirect` route. That's the **same** mechanism as #893's handouts, so no nginx change was needed. - **Tests:** - a non-member, and a member outside a `mine` or `party` map's audience, get 404 for both the image and a tile; - the named member, and the GM, get through; - a tile 404s until it's ready; - #893's nginx proof applies: the public `/media/private/` returns 404. - **Backups:** the nightly media archive tars all of `media_root`, `private/` included, so map images are backed up. The survey's note that they aren't is out of date. ## Departures (for P7) 1. **Tiling is a single zoom level.** Lower-resolution levels for a very large map can be added later without a schema change. 2. **IS-076's rule that a player's marker has three choices** is a P7 UI restriction. The server keeps one audience picker for every surface. 3. **A marker reveal writes no `wiki_events` row,** because `lore_entry_id` is NOT NULL and a marker needn't have a page. If reveals should reach Recent changes, that's P7's feed work. 4. **Atlas nesting and navigation** is P7's. ## Checks - **Full backend suite:** 5785 passed, 0 failed. The only exclusions were the two local-mount artifacts. - **Targeted modules:** 586 passed. - **ruff:** clean. - **Changelog:** a `changelog.d/` fragment. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Migration 8b8c9d0e1f2a on 8a7b8c9d0e1f: atlases, maps, markers, links and
trail stops, all new tables. A map image and its tiles are private files,
stored and served exactly the way a handout's attachment is (media_root/
private/..., X-Accel-Redirect through the existing internal-files route) —
never reachable under nginx's public /media/ alias. app.services.map_audience
enforces the same audience check on every read, including a marker read
inside its map's audience.

The upload cap is its own (map_max_upload_bytes, 75 MB, W9/D195), separate
from the 8 MB every other upload uses; image_service.validate_and_reencode
now takes an optional max_bytes. The tiling task (app.tasks.map_tiles) cuts a
single-resolution 256px pyramid with Pillow, queued through await enqueue()
per #850 — never a direct Celery publish from async code.

Reveal with undo (§3.9): only a GM-only marker is ever revealed, so the undo
always narrows straight back to gm, with nothing else to remember.

Also fixes 8d4e5f6a7b8c's (lane 8 PR 3) downgrade to drop PR 6's new foreign
keys onto parties before dropping the table itself, needed because the test
suite's create_all schema has every table at once and calls each migration's
downgrade in isolation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merge main into build/lane8-6-map
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m49s
CI / Backend lint (ruff) (pull_request) Successful in 1m17s
CI / Bot tests and audit (pull_request) Successful in 5m15s
CI / Feedback site tests (pull_request) Successful in 2m5s
CI / Bot/backend version sync (pull_request) Successful in 1m7s
CI / Docker image build (pull_request) Successful in 33s
CI / Does this change touch the recording path? (pull_request) Successful in 27s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend migration, tests, and audit (pull_request) Successful in 20m4s
CI / Frontend tests, audit, and build (pull_request) Successful in 22m51s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 40m37s
6bdc5cbfbf
Sign in to join this conversation.
No description provided.