Overhaul cloud save system: fix destructive bugs, add save management UI
CI / build-and-push (push) Successful in 57s

Stop 401 responses from wiping local saves and force-reloading. Fix logout
race condition with final cloud save before token invalidation. Replace
hard 3-failure cap with exponential backoff (2min to 30min). Switch cloud
save interval from tick-based (30s) to wall-clock (5min). Add cloud save
status indicator and Force Save button in TopBar. Show save conflict
dialog on login when both local and cloud saves exist. Add cloud save
list, download, and delete in Settings. Server now keeps 10 save snapshots
per user instead of overwriting a single save.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 21:43:00 -04:00
parent 8ef1226755
commit 6ea136083a
10 changed files with 559 additions and 64 deletions
+17 -24
View File
@@ -1,10 +1,12 @@
import { Hono } from 'hono';
import { eq, and, desc } from 'drizzle-orm';
import { eq, and, desc, notInArray } from 'drizzle-orm';
import { db } from '../db';
import { saves } from '../db/schema';
import { authMiddleware } from '../middleware/auth';
import type { AppEnv } from '../types';
const MAX_SAVES_PER_USER = 10;
const savesRouter = new Hono<AppEnv>();
savesRouter.use('*', authMiddleware);
@@ -68,29 +70,6 @@ savesRouter.put('/', async (c) => {
era: string;
}>();
const existing = await db
.select({ id: saves.id })
.from(saves)
.where(eq(saves.userId, userId))
.orderBy(desc(saves.updatedAt))
.limit(1);
if (existing.length > 0) {
await db
.update(saves)
.set({
companyName: body.companyName,
saveVersion: body.saveVersion,
gameData: body.gameData,
tickCount: body.tickCount,
era: body.era,
updatedAt: new Date(),
})
.where(eq(saves.id, existing[0].id));
return c.json({ id: existing[0].id, updated: true });
}
const [newSave] = await db
.insert(saves)
.values({
@@ -103,6 +82,20 @@ savesRouter.put('/', async (c) => {
})
.returning({ id: saves.id });
const keepIds = await db
.select({ id: saves.id })
.from(saves)
.where(eq(saves.userId, userId))
.orderBy(desc(saves.updatedAt))
.limit(MAX_SAVES_PER_USER);
const keepSet = keepIds.map((r) => r.id);
if (keepSet.length === MAX_SAVES_PER_USER) {
await db
.delete(saves)
.where(and(eq(saves.userId, userId), notInArray(saves.id, keepSet)));
}
return c.json({ id: newSave.id, created: true });
});