f2e4259c84
- New themed useConfirm() hook (no native dialogs). World deletes (notes, quests, homebrew, maps) now confirm before removing. - Dice 'Clear history' is disabled with no active campaign (it used to wipe EVERY campaign's rolls) and now confirms; only ever clears the active campaign. - sessionLog is included in backup/restore and the campaign cascade-delete — it was silently dropped on backup and orphaned on campaign delete. - Unknown URLs render a themed 'Page not found' with a link home instead of a blank shell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import type { ZodTypeAny } from 'zod';
|
|
import { db } from '@/lib/db/db';
|
|
import {
|
|
campaignSchema, characterSchema, encounterSchema, diceRollSchema,
|
|
noteSchema, npcSchema, questSchema, calendarSchema, battleMapSchema, homebrewSchema,
|
|
sessionLogEntrySchema,
|
|
} from '@/lib/schemas';
|
|
import { downloadJson } from './file';
|
|
|
|
const TABLES = [
|
|
'campaigns', 'characters', 'encounters', 'diceRolls',
|
|
'notes', 'npcs', 'quests', 'calendars', 'maps', 'homebrew', 'sessionLog',
|
|
] as const;
|
|
|
|
/** Schema per table, so restored rows are validated + backfilled with defaults
|
|
* (a backup from an older app version is missing newer fields, e.g. `feats`). */
|
|
const SCHEMA: Record<(typeof TABLES)[number], ZodTypeAny> = {
|
|
campaigns: campaignSchema, characters: characterSchema, encounters: encounterSchema,
|
|
diceRolls: diceRollSchema, notes: noteSchema, npcs: npcSchema, quests: questSchema,
|
|
calendars: calendarSchema, maps: battleMapSchema, homebrew: homebrewSchema,
|
|
sessionLog: sessionLogEntrySchema,
|
|
};
|
|
|
|
const FORMAT = 'ttrpg-manager:backup';
|
|
|
|
export class BackupError extends Error {}
|
|
|
|
/** Snapshot every table into a portable object. */
|
|
export async function buildBackup(): Promise<{ format: string; version: number; data: Record<string, unknown[]> }> {
|
|
const data: Record<string, unknown[]> = {};
|
|
for (const t of TABLES) data[t] = await db.table(t).toArray();
|
|
return { format: FORMAT, version: 1, data };
|
|
}
|
|
|
|
/** Download a full backup of all campaigns and data. */
|
|
export async function exportBackup(): Promise<void> {
|
|
downloadJson('ttrpg-manager-backup.json', await buildBackup());
|
|
}
|
|
|
|
/**
|
|
* Restore a backup, REPLACING all current data. Runs in one transaction so a
|
|
* failure rolls back cleanly.
|
|
*/
|
|
export async function restoreBackup(text: string): Promise<void> {
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch {
|
|
throw new BackupError('That file is not valid JSON.');
|
|
}
|
|
const data = (parsed as { data?: Record<string, unknown[]> })?.data;
|
|
if (!data || typeof data !== 'object') throw new BackupError('That file is not a TTRPG Manager backup.');
|
|
|
|
await db.transaction('rw', TABLES.map((t) => db.table(t)), async () => {
|
|
for (const t of TABLES) {
|
|
await db.table(t).clear();
|
|
const rows = data[t];
|
|
if (!Array.isArray(rows) || !rows.length) continue;
|
|
// Validate + backfill defaults so a backup from an older app version (or an
|
|
// edited/foreign file) can't land rows missing newer fields and crash the UI.
|
|
const valid: unknown[] = [];
|
|
for (const row of rows) {
|
|
const r = SCHEMA[t].safeParse(row);
|
|
if (r.success) valid.push(r.data);
|
|
}
|
|
if (valid.length) await db.table(t).bulkAdd(valid);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Wipe all data (danger zone). */
|
|
export async function clearAllData(): Promise<void> {
|
|
await db.transaction('rw', TABLES.map((t) => db.table(t)), async () => {
|
|
for (const t of TABLES) await db.table(t).clear();
|
|
});
|
|
}
|