feat(export): carry tasks and amounts in the backup
They exist on this device and nowhere else, so an export without them is a lossy backup. Both are additive keys, as custom games were: older files lack them and still import. Tasks travel as stored, including any this build cannot read. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c0a0b94054
commit
e97e8b002f
+21
-4
@@ -17,6 +17,8 @@ import { useAppUpdate } from "./state/useAppUpdate.ts";
|
||||
import { useMarkSet } from "./state/useMarkSet.ts";
|
||||
import { useProgress } from "./state/useProgress.ts";
|
||||
import { useDailyLog, type DailyLogMap } from "./state/useDailyLog.ts";
|
||||
import { useTasks } from "./state/useTasks.ts";
|
||||
import { useAmounts } from "./state/useAmounts.ts";
|
||||
import { adoptNewLanes, usePrefs, type Prefs } from "./state/usePrefs.ts";
|
||||
import { buildExportData, parseImportData, triggerDownload } from "./state/export.ts";
|
||||
import { snapDayWidth } from "./state/zoom.ts";
|
||||
@@ -39,8 +41,6 @@ import { metaOnTheme, useTheme } from "./state/theme.ts";
|
||||
import {
|
||||
displayEventFor,
|
||||
recordFor,
|
||||
type CustomEvents,
|
||||
type CustomGames,
|
||||
type LaneId,
|
||||
} from "../shared/custom.ts";
|
||||
import { metaFor } from "../shared/games.ts";
|
||||
@@ -113,6 +113,12 @@ export function App() {
|
||||
const ignored = useMarkSet(KEYS.ignored);
|
||||
const prog = useProgress();
|
||||
const daily = useDailyLog();
|
||||
const tasks = useTasks();
|
||||
const amounts = useAmounts();
|
||||
const mergeExtra = (incomingTasks: unknown, incomingAmounts: unknown) => {
|
||||
tasks.merge(incomingTasks);
|
||||
amounts.merge(incomingAmounts);
|
||||
};
|
||||
const custom = useCustom(now);
|
||||
// Colour only: which ground the page is drawn on, written to the document by
|
||||
// the hook. Nothing else in the app asks what it is — the tokens in
|
||||
@@ -687,6 +693,8 @@ export function App() {
|
||||
exportProgress(prog.progress, daily.logs, ignored.marks, {
|
||||
games: custom.games,
|
||||
events: custom.events,
|
||||
tasks: tasks.raw,
|
||||
amounts: amounts.amounts,
|
||||
})
|
||||
}
|
||||
onImport={(file) =>
|
||||
@@ -696,12 +704,15 @@ export function App() {
|
||||
daily.merge,
|
||||
ignored.merge,
|
||||
custom.merge,
|
||||
mergeExtra,
|
||||
)
|
||||
}
|
||||
onExportAll={() =>
|
||||
exportAll(prog.progress, daily.logs, ignored.marks, prefs, {
|
||||
games: custom.games,
|
||||
events: custom.events,
|
||||
tasks: tasks.raw,
|
||||
amounts: amounts.amounts,
|
||||
})
|
||||
}
|
||||
onImportAll={(file) =>
|
||||
@@ -711,6 +722,7 @@ export function App() {
|
||||
daily.merge,
|
||||
ignored.merge,
|
||||
custom.merge,
|
||||
mergeExtra,
|
||||
restorePrefs,
|
||||
)
|
||||
}
|
||||
@@ -942,7 +954,7 @@ function exportProgress(
|
||||
progress: Record<string, unknown>,
|
||||
daily: DailyLogMap,
|
||||
ignored: Record<string, { at: string }>,
|
||||
own: { games: CustomGames; events: CustomEvents },
|
||||
own: Own,
|
||||
) {
|
||||
const data = buildExportData(progress, daily, ignored, own);
|
||||
triggerDownload(
|
||||
@@ -956,7 +968,7 @@ function exportAll(
|
||||
daily: DailyLogMap,
|
||||
ignored: Record<string, { at: string }>,
|
||||
prefs: Prefs,
|
||||
own: { games: CustomGames; events: CustomEvents },
|
||||
own: Own,
|
||||
) {
|
||||
const data = buildExportData(progress, daily, ignored, own, prefs);
|
||||
triggerDownload(
|
||||
@@ -965,12 +977,16 @@ function exportAll(
|
||||
);
|
||||
}
|
||||
|
||||
/** Everything reader-authored that travels in an export beside progress. */
|
||||
type Own = Parameters<typeof buildExportData>[3];
|
||||
|
||||
async function importProgress(
|
||||
file: File,
|
||||
mergeProgress: (c: Record<string, { at: string }>) => void,
|
||||
mergeDaily: (c: DailyLogMap) => void,
|
||||
mergeIgnored: (c: Record<string, { at: string }>) => void,
|
||||
mergeCustom: (games: unknown, events: unknown) => void,
|
||||
mergeExtra: (tasks: unknown, amounts: unknown) => void,
|
||||
restorePrefs?: ((prefs: unknown) => void) | undefined,
|
||||
) {
|
||||
try {
|
||||
@@ -984,6 +1000,7 @@ async function importProgress(
|
||||
if (data.daily !== null) mergeDaily(data.daily);
|
||||
if (data.ignored !== null) mergeIgnored(data.ignored);
|
||||
mergeCustom(data.customGames, data.customEvents);
|
||||
mergeExtra(data.tasks, data.amounts);
|
||||
|
||||
if (restorePrefs !== undefined) {
|
||||
if (data.prefs !== null) {
|
||||
|
||||
@@ -12,6 +12,10 @@ export interface ExportData {
|
||||
ignored: Record<string, { at: string }>;
|
||||
customGames: CustomGames;
|
||||
customEvents: CustomEvents;
|
||||
/** Weekly and monthly tasks, as stored — readable to this build or not. */
|
||||
tasks?: Record<string, unknown> | undefined;
|
||||
/** Numbers entered against targets, per item per period. */
|
||||
amounts?: Record<string, Record<string, unknown>> | undefined;
|
||||
prefs?: Prefs | undefined;
|
||||
}
|
||||
|
||||
@@ -21,6 +25,9 @@ export interface ParsedImport {
|
||||
ignored: Record<string, { at: string }> | null;
|
||||
customGames: unknown;
|
||||
customEvents: unknown;
|
||||
/** Validated by the task and amount stores' own merges, not here. */
|
||||
tasks: unknown;
|
||||
amounts: unknown;
|
||||
prefs: unknown | null;
|
||||
}
|
||||
|
||||
@@ -33,7 +40,12 @@ export function buildExportData(
|
||||
progress: Record<string, unknown>,
|
||||
daily: DailyLogMap,
|
||||
ignored: Record<string, { at: string }>,
|
||||
own: { games: CustomGames; events: CustomEvents },
|
||||
own: {
|
||||
games: CustomGames;
|
||||
events: CustomEvents;
|
||||
tasks?: Record<string, unknown> | undefined;
|
||||
amounts?: Record<string, Record<string, unknown>> | undefined;
|
||||
},
|
||||
prefs?: Prefs | undefined,
|
||||
): ExportData {
|
||||
return {
|
||||
@@ -45,6 +57,10 @@ export function buildExportData(
|
||||
ignored,
|
||||
customGames: own.games,
|
||||
customEvents: own.events,
|
||||
// Additive keys, like customGames was: a file without them is simply from
|
||||
// a device that had none.
|
||||
...(own.tasks !== undefined ? { tasks: own.tasks } : {}),
|
||||
...(own.amounts !== undefined ? { amounts: own.amounts } : {}),
|
||||
...(prefs !== undefined ? { prefs } : {}),
|
||||
};
|
||||
}
|
||||
@@ -63,6 +79,8 @@ export function parseImportData(parsed: unknown): ParsedImport | null {
|
||||
ignored?: unknown;
|
||||
customGames?: unknown;
|
||||
customEvents?: unknown;
|
||||
tasks?: unknown;
|
||||
amounts?: unknown;
|
||||
prefs?: unknown;
|
||||
};
|
||||
if (data.format !== "gacha-tracker-export") return null;
|
||||
@@ -98,6 +116,8 @@ export function parseImportData(parsed: unknown): ParsedImport | null {
|
||||
ignored,
|
||||
customGames: data.customGames,
|
||||
customEvents: data.customEvents,
|
||||
tasks: data.tasks,
|
||||
amounts: data.amounts,
|
||||
prefs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -234,3 +234,27 @@ describe("a goal this build cannot read is never cleared by accident", () => {
|
||||
expect(dailyPatch(false, true)).toStrictEqual({ daily: false, goal: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe("tasks and amounts in the export", () => {
|
||||
const T = "2026-10-13T12:00:00.000Z";
|
||||
const tasks = {
|
||||
"mytask:aaaaaa1111": { id: "mytask:aaaaaa1111", game: "zzz", title: "Activity" },
|
||||
// Unreadable to this build, and carried anyway: the export is a backup.
|
||||
"mytask:cccccc3333": { id: "mytask:cccccc3333", measure: { kind: "streak" } },
|
||||
};
|
||||
const amounts = { "mytask:aaaaaa1111": { "2026-10-12": { value: 1900, at: T } } };
|
||||
|
||||
test("a round trip carries both, readable or not", () => {
|
||||
const data = buildExportData({}, {}, {}, { games: {}, events: {}, tasks, amounts });
|
||||
const parsed = parseImportData(JSON.parse(JSON.stringify(data)))!;
|
||||
expect(parsed.tasks).toEqual(tasks);
|
||||
expect(parsed.amounts).toEqual(amounts);
|
||||
});
|
||||
|
||||
test("an export from before tasks has neither, and is still valid", () => {
|
||||
const parsed = parseImportData({ format: "gacha-tracker-export", version: 1, progress: {} })!;
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed.tasks).toBeUndefined();
|
||||
expect(parsed.amounts).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user