Compare commits

..

3 Commits

Author SHA1 Message Date
planner-bot 8551d14591 docs: MVP vertical slice planning doc for client-server wiring
- Architecture task: ApiClient autoload (HTTP + JWT + error contract)
- Implementation task: Login/Register UI wired to /api/auth
- Implementation task: Browse worlds screen wired to GET /api/worlds
- Validation task: End-to-end smoke test (login + list worlds)
2026-08-19 12:39:33 +00:00
agent-bot ff610c0466 Merge pull request 'Fix migration 0001_initial double CREATE TYPE + port collision' (#25) from fix/migration-0001-double-enum-and-port-collision into main 2026-08-18 21:57:01 +00:00
claude-mvp-fix acbc518b4a Fix migration 0001_initial: double CREATE TYPE crashes every clean bootstrap
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.
2026-08-18 21:55:34 +00:00
5 changed files with 42 additions and 6 deletions
+3 -2
View File
@@ -69,7 +69,8 @@ def upgrade() -> None:
sa.Column( sa.Column(
"visibility", "visibility",
postgresql.ENUM( postgresql.ENUM(
"public", "private", "friends_only", name="world_visibility" "public", "private", "friends_only", name="world_visibility",
create_type=False,
), ),
nullable=True, nullable=True,
), ),
@@ -89,7 +90,7 @@ def upgrade() -> None:
"asset_type", "asset_type",
postgresql.ENUM( postgresql.ENUM(
"image", "audio", "video", "document", "model_3d", "other", "image", "audio", "video", "document", "model_3d", "other",
name="asset_type", name="asset_type", create_type=False,
), ),
nullable=False, nullable=False,
), ),
+2 -2
View File
@@ -8,7 +8,7 @@ services:
POSTGRES_PASSWORD: agp_password POSTGRES_PASSWORD: agp_password
POSTGRES_DB: agp_db POSTGRES_DB: agp_db
ports: ports:
- "5432:5432" - "${POSTGRES_HOST_PORT:-5432}:5432"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
healthcheck: healthcheck:
@@ -20,7 +20,7 @@ services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
ports: ports:
- "6379:6379" - "${REDIS_HOST_PORT:-6379}:6379"
volumes: volumes:
- redis_data:/data - redis_data:/data
healthcheck: healthcheck:
+37
View File
@@ -0,0 +1,37 @@
# MVP Vertical Slice: Client-Server Wiring — Plan
## Goal
Prove the real Godot client and the real backend can talk: **a user can register/log in
and see a list of worlds from the game client**. Nothing more. Gameplay, join-a-world,
chat, avatar, Discovery search/tags/trending, Social/Economy/Trust & Safety are all
explicitly out of scope.
## Verified backend contract (as of this writing)
- `POST /api/auth/register` — JSON body `{username, email, password, date_of_birth}`.
`date_of_birth` is a `YYYY-MM-DD` string. Returns `201 {access_token, token_type}`.
Rejects under-18 with `400`.
- `POST /api/auth/login` — OAuth2 **form-encoded** (`application/x-www-form-urlencoded`)
`username` + `password` (this is NOT a JSON endpoint). Returns `{access_token, token_type}`.
- `GET /api/worlds` — **requires auth** (`CurrentUser`), returns a JSON array of
`{id, name, description, visibility, owner_id, created_at}`. There is no unauthenticated
public-worlds path, so the worlds list screen can only be shown after login/register.
- `GET /api/health` — unauthenticated, `{status, app}`.
- Base URL: uvicorn default `http://127.0.0.1:8000` (must be configurable in the client).
- CORS is irrelevant to the native Godot `HTTPRequest` client (no browser origin).
## Task breakdown
| # | Type | Task | Depends on |
|---|------|------|-----------|
| 1 | architecture | `ApiClient` autoload: HTTP + JWT storage + error contract | — |
| 2 | implementation | Main-menu **login** wiring (form-encoded) | 1 |
| 3 | implementation | Main-menu **register** wiring (JSON + age gate) | 2 |
| 4 | implementation | **World list** screen consuming `GET /api/worlds` | 2 |
| 5 | testing | End-to-end headless smoke test + manual verify | 1,3,4 |
## Design decisions captured (resolved in task 1)
- Token storage: in-memory singleton property for the MVP; a `save_to_disk` seam is left
open but not required yet (no re-login on restart in this slice).
- Error contract: every call returns a discriminated result (`ok`/`error`) so callers
never crash on network failure; non-2xx surfaces `{status, detail}` from the backend.
- Base URL: read from a ProjectSetting, defaulting to `http://127.0.0.1:8000`.
- Login is form-encoded; register is JSON — the client helper must support both content types.
-1
View File
@@ -1 +0,0 @@
uid://ctqseajls16vv
-1
View File
@@ -1 +0,0 @@
uid://bqp1qfc6by0xs