Splash case picker + playable-mystery list

GET /api/mysteries returns only mysteries with an entrypoint (empties are
filtered out), and the splash now lists them as case files with a per-case
BEGIN / RESUME action instead of a single hardcoded New Game.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 15:51:32 +02:00
co-authored by Claude Opus 4.8
parent cd4b8bf4fa
commit 5c514562a2
4 changed files with 56 additions and 10 deletions
+9
View File
@@ -51,6 +51,7 @@ export interface NarrativeRepository {
awardAchievement(playthroughId: string, flagKey: string, nodeId?: string | null): Promise<{ ok: boolean; earned: boolean; error?: string }>
gotoNode(userId: string, playthroughId: string, nodeId: string): Promise<{ ok: boolean; state?: PlaythroughState; error?: string }>
listMysteries(): Promise<MysterySummary[]>
listPlayableMysteries(): Promise<{ slug: string; title: string }[]>
deleteMystery(id: string): Promise<boolean>
uploadAsset(file: UploadedFile): Promise<AssetDto>
listAssets(): Promise<AssetDto[]>
@@ -304,6 +305,14 @@ export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStora
return { ok: true, state: state ?? undefined }
},
// Play mode: only mysteries with an entrypoint are launchable (this filters out
// half-authored, empty ones). Returns the minimum the case picker needs.
async listPlayableMysteries() {
const result = await pool.query<{ slug: string; title: string }>(
'SELECT slug,title FROM osint.mysteries WHERE entry_node_id IS NOT NULL ORDER BY title')
return result.rows.map(row => ({ slug: row.slug, title: row.title }))
},
async listMysteries() {
const result = await pool.query<{ id: string; slug: string; title: string; nodes: string }>(
`SELECT m.id,m.slug,m.title,COUNT(n.id)::text AS nodes