Recovers a real August feature that was never merged (3 WIP checkpoints +
fix commit 47ab492, 225 commits behind main): a Redis-backed heartbeat
presence store plus /api/presence/{heartbeat,presence,status} endpoints.
Ported onto current main's conventions rather than rebuilt:
- app/models/user.py: last_seen_at/status_message columns (unchanged from
the orphaned branch; still compatible with the current model).
- alembic/versions/0006_add_presence_columns.py: same column migration,
rebased as the new head after 0005_add_world_content_tables (was
2a9b9873244a off 0001_initial).
- app/routers/presence.py: same three endpoints, updated to the DbDep
pattern other routers (avatar.py) now use; retains the 47ab492 fix
(is_online_batch via MGET, correct StatusUpdate Field annotation).
- app/services/presence.py: real adaptation beyond a mechanical port --
the original PresenceStore required an explicit connect() that nothing
ever called (main.py's lifespan never wired it up), so every real
request would have raised "not connected". Made the Redis client connect
lazily on first use instead, and cache it per running event loop rather
than globally: this repo's TestClient(app) pattern (no `with` block) can
dispatch different requests onto different event loops, which broke a
single cached connection the same way NullPool's comment in conftest.py
describes for asyncpg. Dropped the unused get_presence()/
update_status_message() methods and the never-wired init/shutdown
singleton helpers -- dead code even in the original.
- tests/conftest.py: extended the existing ephemeral-Postgres bootstrap to
also bring up ephemeral Redis (docker-compose.yml already defined the
service) when REDIS_URL isn't already reachable, so `pytest` stays
hermetic.
- tests/test_presence.py: heartbeat sets online, batch presence over
mixed online/offline/unknown ids (the exact 47ab492 regression), status
message update/strip/length-limit, and TTL expiry via a short-TTL
PresenceStore instance (real Redis SET/EXISTS/MGET, no 60s sleep needed).
Verified: full backend pytest suite (144 tests) passes against a fresh
Postgres+Redis. Also drove the real endpoints end-to-end against a running
uvicorn + docker-compose Postgres/Redis: heartbeat -> online, batch query,
forced TTL expiry -> offline with last_seen_at retained, status message
trim + 422 over 140 chars.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2FESCaMXeJpC9gSctiEeD
Implements docs/architecture/world_persistence_impl.md §9's backend task:
chunk/room/revision Postgres schema (migration 0005), a Python port of the
world.md §3.6 binary chunk codec with golden-file tests, a WorldStore
service (world.md §5.1 minus flush_dirty/rebuild_index), the public
world-content router (rooms/chunks CRUD, optimistic concurrency, batch
writes) and the internal read-only API for dedicated servers, and
game_servers.py changes: a per-spawn bearer token, --api-url/--server-token
args, and a real "ready"-field wait replacing _await_port, which bound the
UDP port before content existed and failed open (no error) past its
deadline.
create_world now auto-creates the "main" room.
Deviations/findings, not silently decided:
- chunk_codec.py implements real IEEE-754 float32 packing rather than
mirroring a confirmed bug in scripts/chunk_data_decoder.gd's
_read_float32/_write_float32, which do a lossy int<->float cast instead
of a bit reinterpretation (verified against Godot 4.6.2 headless) and
will corrupt any non-integer position/rotation/scale/bounds value. Not
fixed here (client-side, out of scope); flagged for the client task.
- world_persistence_impl.md references a world's monotonic
content_revision (§6/§7.2/§8.1) but never defines where it's stored.
Resolved as MAX(id) over the append-only world_chunk_revisions table,
scoped to world_id, rather than adding an undocumented column.
- No existing Godot-side golden fixture/test vectors were found to reuse,
so tests/fixtures/chunks/*.bin are authored fresh from this
implementation (see generate_fixtures.py).
Verification: 132/132 backend pytest tests pass against a real ephemeral
Postgres (both reusing an already-running instance and cold via
conftest.py's own docker-compose bootstrap), including migration
upgrade/downgrade/upgrade, codec round-trip + validation-error tests, and
full HTTP round trips for PUT/GET/batch/delete/revisions/rollback and the
internal API's token auth.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2FESCaMXeJpC9gSctiEeD
New avatar_loadouts table (one row per user, loadout stored as a single
JSONB document matching the client's AvatarLoadout.to_json() shape) and
GET/PUT /api/avatar/loadout.
The server has no copy of the asset catalog, so it validates only the
envelope (JSON object, non-empty base_body, object slots, size cap) --
which-asset-in-which-slot validation stays client-side. New accounts get a
server default (base_human_male_01, no slots) with is_default:true so the
client can tell "never customised" from "customised back to base".
- models/avatar_loadout.py + 0004 migration + User.avatar_loadout relation
- routers/avatar.py wired into main.py
- tests/test_avatar_loadout.py (7 tests: default, save/read, replace,
validation, auth) -- full suite 68 passed
- lifted the duplicated _override_db fixture from 3 test files into
conftest.py (new test files need it too)
- docs/architecture/avatar.md §9a: the persistence design
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
world_visibility and asset_type were each declared twice: once explicitly
via `.create(checkfirst=True)`, and again implicitly by SQLAlchemy when
create_table() compiled the inline postgresql.ENUM() column type (which
defaults create_type=True). On a genuinely empty database this raises
asyncpg.DuplicateObjectError mid-transaction and the migration never
completes -- verified this was true 100% of the time from a fresh
container, meaning the backend has never successfully bootstrapped from
scratch. Fixed by passing create_type=False on the inline column
declarations, since the type is already created explicitly above.
Also parameterized both services' host ports (POSTGRES_HOST_PORT /
REDIS_HOST_PORT, defaulting to the original 5432/6379) so concurrent
agent workspaces sharing this same docker-compose.yml can run isolated
databases without colliding on the host port.