feat: remove a saved game & redirect on missing/deleted game (#20)
CI / build + unit + e2e (pull_request) Successful in 1m14s

- DELETE /api/games/:id route + Db.delete() (404 when absent)
- PresetPicker: per-saved-game trash button + confirm dialog (Cancel/Delete)
- App: runDeleteGame; useSse onError -> goHome on 'deleted'/'not found'
  (a deleted/missing game now returns the live tab and any deep link home)
- DeleteGameResponse in shared/protocol.ts; api.deleteGame()
- unit test (Db.delete) + 2 e2e tests (home-screen delete; in-flight delete
  returns the active tab home and a fresh deep link lands on home)
- version 0.7.0 -> 0.8.0 + changelog

Fixes #20
This commit is contained in:
2026-10-11 20:32:55 +00:00
parent f05ddb304d
commit 17d6cecab9
11 changed files with 250 additions and 7 deletions
+15
View File
@@ -72,4 +72,19 @@ describe('openDb', () => {
expect(db.load('missing')).toBeNull();
db.close();
});
it('delete removes a game by id and reports absence otherwise', () => {
const db = openDb(':memory:');
db.save(makeState('a'));
db.save(makeState('b'));
expect(db.delete('nope')).toBe(false); // unknown id
expect(db.delete('a')).toBe(true); // removes a
expect(db.load('a')).toBeNull();
expect(db.load('b')?.id).toBe('b');
expect([...db.listIds()].sort()).toEqual(['b']);
expect(db.delete('a')).toBe(false); // already gone
db.close();
});
});
+5
View File
@@ -8,6 +8,7 @@ export interface Db {
save(state: GameState): void;
load(id: string): GameState | null;
listIds(): string[];
delete(id: string): boolean;
close(): void;
}
@@ -29,6 +30,7 @@ export function openDb(filePath: string): Db {
);
const byId = db.prepare('SELECT data FROM games WHERE id = ?');
const all = db.prepare('SELECT id FROM games ORDER BY updated_at DESC');
const del = db.prepare('DELETE FROM games WHERE id = ?');
return {
save(state) {
upsert.run(state.id, JSON.stringify(state), new Date().toISOString());
@@ -45,6 +47,9 @@ export function openDb(filePath: string): Db {
listIds() {
return (all.all() as { id: string }[]).map((r) => r.id);
},
delete(id) {
return del.run(id).changes > 0;
},
close() {
db.close();
},
+11
View File
@@ -156,6 +156,17 @@ app.get('/api/games/:id/map', (req, res) => {
res.json({ regions: buildRegionMap(game) });
});
// Delete a saved game entirely. Fast, non-LLM. 404 if it doesn't exist; on
// success broadcast an SSE `error` to any other open tab watching it so it
// returns home (see the `event: error` handling in src/lib/sse.ts).
app.delete('/api/games/:id', (req, res) => {
const id = req.params.id;
const removed = db.delete(id);
if (!removed) return res.status(404).json({ error: 'game not found' });
broadcast(id, 'error', { message: 'game deleted' });
res.json({ deleted: true });
});
// Queue a free-text player action onto the pending round. No LLM call and no
// turn advance — the round is only committed (in one turn) by "Jump forward".
app.post('/api/games/:id/action', (req, res) => {