feat: migrate graph snapshot handling to localStorage for improved persistence
All checks were successful
On Push to Main / test (push) Successful in 39s
On Push to Main / publish (push) Successful in 1m7s
On Push to Main / deploy-dev (push) Successful in 1m36s

This commit is contained in:
RaymondVerhoef
2026-05-27 20:27:16 +02:00
parent 9fb22b8090
commit 47a738fde3
2 changed files with 44 additions and 35 deletions

View File

@@ -293,44 +293,52 @@ export function setSetting(key, value) {
// ── Graph Snapshots ───────────────────────────────────────────────────────────
//
// A single "latest" snapshot is persisted in the settings collection before
// every bulk graph write. This survives browser/tab close and lets the admin
// roll back an AI analysis that produced unwanted results.
// Stored in localStorage, not in PocketBase.
//
// The value is stored as a JSON string because the settings collection only
// has a plain text `value` field. Large graphs (150+ topics) can produce
// ~100 KB of JSON, which SQLite handles without issue.
// The settings collection's `value` field is a plain text field designed for
// small strings (model overrides, flags). A full graph snapshot can easily
// reach 40100 KB of JSON, which causes a 400 Bad Request from PocketBase.
//
// localStorage is the right choice here: the admin who clicks "Analyze" and
// the admin who clicks "Restore" are always the same person, on the same
// machine, in the same browser. Cross-device persistence is not required.
const SNAPSHOT_KEY = 'graph_snapshot:latest';
const SNAPSHOT_LS_KEY = 'respellion:graph_snapshot';
/**
* Persist the current graph state as a rollback point.
* Persist the current graph state as a rollback point in localStorage.
* Called by useGraphData.bulkSave() before overwriting the graph.
*
* @param {object[]} topics — the graph state to preserve
* @param {object[]} relations — the graph state to preserve
* Silently no-ops if localStorage is unavailable (private browsing quota
* exceeded, etc.) — the snapshot is best-effort, not critical-path.
*
* @param {object[]} topics
* @param {object[]} relations
*/
export async function saveGraphSnapshot(topics, relations) {
const payload = JSON.stringify({
ts: Date.now(),
topicCount: topics.length,
relationCount: relations.length,
topics,
relations,
});
return setSetting(SNAPSHOT_KEY, payload);
export function saveGraphSnapshot(topics, relations) {
try {
localStorage.setItem(SNAPSHOT_LS_KEY, JSON.stringify({
ts: Date.now(),
topicCount: topics.length,
relationCount: relations.length,
topics,
relations,
}));
} catch {
// Storage quota exceeded or unavailable — skip silently.
}
}
/**
* Read the latest snapshot back from PocketBase.
* Returns null when no snapshot exists or the stored value is malformed.
* Read the latest snapshot from localStorage.
* Returns null when nothing is stored or the value is malformed.
*
* @returns {{ ts: number, topicCount: number, relationCount: number, topics: object[], relations: object[] } | null}
*/
export async function getGraphSnapshot() {
const raw = await getSetting(SNAPSHOT_KEY, null);
if (!raw) return null;
export function getGraphSnapshot() {
try {
const raw = localStorage.getItem(SNAPSHOT_LS_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch {
return null;
@@ -338,11 +346,13 @@ export async function getGraphSnapshot() {
}
/**
* Delete the stored snapshot (e.g. after a successful restore so the
* admin cannot accidentally restore twice).
* Remove the stored snapshot after a successful restore so the admin cannot
* accidentally restore twice.
*/
export function clearGraphSnapshot() {
return setSetting(SNAPSHOT_KEY, '');
try {
localStorage.removeItem(SNAPSHOT_LS_KEY);
} catch { /* non-fatal */ }
}
// ── Curriculum Versions (v2) ──────────────────────────────────────────────────