mirror of
https://github.com/Heretek-AI/AetherTable.git
synced 2026-10-12 02:02:28 +00:00
- storage.py: dual-mode store — PostgresStore (asyncpg pool) when
DATABASE_URL is reachable, MemoryStore fallback otherwise; idempotent
_ensure_tables() DDL at startup; PBKDF2 credential helpers centralized
- 03_campaign_persistence.sql: narrative_state.users, campaigns (anchors
the previously free-floating campaign_id), campaign_saves (JSONB
snapshots) + owner/updated index — also auto-run by compose init
- auth routes now persist users across restarts (verified live: seeded GM
account survives process restarts via Postgres)
- new token-guarded API: POST /api/v1/campaign/save (upsert by owner+name),
GET /campaign/saves, GET /campaign/save/{id}, DELETE /campaign/save/{id}
- client: CampaignSaveModal (save/list/load/delete with round + timestamp),
Campaign Saves entry in Table Tools dropdown, snapshot build/apply in App
- fix compose postgres crash-loop on SELinux systems (:ro,z relabel flag)
- tests: memory-mode API CRUD always runs; PG end-to-end incl. cross-owner
denial and durability-across-reinit skips without DATABASE_URL
(58 pytest pass with Postgres up, cargo clean, MCR 100%)
Co-Authored-By: Claude <[email protected]>
41 lines
1.7 KiB
SQL
41 lines
1.7 KiB
SQL
-- Wave 12: Campaign Persistence
|
|
-- Idempotent DDL for identity and campaign save/load. The orchestrator also
|
|
-- executes this at startup (storage.py::_ensure_tables) because the docker
|
|
-- compose init directory only runs on first volume creation.
|
|
|
|
CREATE SCHEMA IF NOT EXISTS narrative_state;
|
|
|
|
CREATE TABLE IF NOT EXISTS narrative_state.users (
|
|
user_id TEXT PRIMARY KEY,
|
|
email TEXT UNIQUE NOT NULL,
|
|
username TEXT NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'player'
|
|
CHECK (role IN ('gm', 'player', 'spectator', 'admin')),
|
|
password_hash TEXT NOT NULL,
|
|
salt_hex TEXT NOT NULL,
|
|
assigned_token_ids JSONB NOT NULL DEFAULT '[]',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS narrative_state.campaigns (
|
|
campaign_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
owner_user_id TEXT NOT NULL REFERENCES narrative_state.users(user_id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS narrative_state.campaign_saves (
|
|
save_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
campaign_id UUID REFERENCES narrative_state.campaigns(campaign_id) ON DELETE CASCADE,
|
|
owner_user_id TEXT NOT NULL REFERENCES narrative_state.users(user_id) ON DELETE CASCADE,
|
|
save_name TEXT NOT NULL,
|
|
snapshot JSONB NOT NULL,
|
|
round_number INT NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_campaign_saves_owner_updated
|
|
ON narrative_state.campaign_saves (owner_user_id, updated_at DESC);
|