feat: implement snapshot restore functionality and enhance graph state management
All checks were successful
On Push to Main / test (push) Successful in 40s
On Push to Main / publish (push) Successful in 1m10s
On Push to Main / deploy-dev (push) Successful in 1m35s

This commit is contained in:
RaymondVerhoef
2026-05-27 17:43:18 +02:00
parent 6ea8860b96
commit 6309ae716b
4 changed files with 164 additions and 11 deletions

View File

@@ -10,11 +10,20 @@ import * as db from '../lib/db';
* Relation shape contract: source and target are always plain strings.
* The normalization in db.getRelations() enforces this at the DB boundary;
* the filter here drops any orphaned edges whose endpoints no longer exist.
*
* Snapshot contract:
* bulkSave() writes a snapshot of the CURRENT state to PocketBase before
* overwriting it — so the admin can always roll back one step. snapshotMeta
* is non-null whenever a rollback point exists; restoreSnapshot() applies it
* and then clears it so it cannot be applied twice.
*/
export function useGraphData() {
const [topics, setTopics] = useState([]);
const [relations, setRelations] = useState([]);
// { ts: number, topicCount: number, relationCount: number }
const [snapshotMeta, setSnapshotMeta] = useState(null);
// Refs that always hold the latest state — lets mutation callbacks avoid
// stale-closure bugs without including state in their dependency arrays.
const topicsRef = useRef([]);
@@ -41,11 +50,19 @@ export function useGraphData() {
return () => window.removeEventListener('respellion:kb-updated', reload);
}, [reload]);
// On mount, check whether a snapshot from a previous session exists.
useEffect(() => {
db.getGraphSnapshot().then(snap => {
if (snap?.ts) {
setSnapshotMeta({ ts: snap.ts, topicCount: snap.topicCount, relationCount: snap.relationCount });
}
});
}, []);
// ── Single-topic mutations ───────────────────────────────────────────────────
/**
* Persist a topic update and mirror it into local state.
* Automatically locks learning_relevance when the admin changes it.
* Returns the saved topic object.
*/
const updateTopic = useCallback(async (topic) => {
@@ -74,7 +91,7 @@ export function useGraphData() {
/**
* Add a relation if not already present.
* Returns true if the relation was added, false if it was a duplicate.
* Returns true if added, false if it was a duplicate.
*/
const addRelation = useCallback(async (relation) => {
const isDup = relationsRef.current.some(
@@ -99,24 +116,60 @@ export function useGraphData() {
// ── Bulk mutations (used by AI analysis) ────────────────────────────────────
/**
* Atomically replace the entire graph in PocketBase and local state.
* Called by analyzeGraph after all AI actions have been applied locally.
* Snapshot the current graph then replace everything in PocketBase and local
* state. The snapshot is written BEFORE the destructive delete-all/create-all
* so it survives a mid-operation failure.
*
* Called by analyzeGraph after all AI actions have been applied in memory.
*/
const bulkSave = useCallback(async (newTopics, newRelations) => {
// Persist a rollback point of the state we are about to overwrite.
await db.saveGraphSnapshot(topicsRef.current, relationsRef.current);
setSnapshotMeta({
ts: Date.now(),
topicCount: topicsRef.current.length,
relationCount: relationsRef.current.length,
});
await db.saveTopics(newTopics);
await db.saveRelations(newRelations);
setTopics(newTopics);
setRelations(newRelations);
}, []);
/**
* Load the latest snapshot from PocketBase and apply it as the current graph.
* Clears the snapshot afterwards so the admin cannot restore the same point
* twice (which would silently re-apply the AI changes they just rolled back).
*
* Returns true on success, false if no snapshot exists.
*/
const restoreSnapshot = useCallback(async () => {
const snap = await db.getGraphSnapshot();
if (!snap?.topics) return false;
// Apply without taking a new snapshot — we don't want to overwrite
// the only rollback point we have.
await db.saveTopics(snap.topics);
await db.saveRelations(snap.relations);
setTopics(snap.topics);
setRelations(snap.relations);
await db.clearGraphSnapshot();
setSnapshotMeta(null);
return true;
}, []);
return {
topics,
relations,
snapshotMeta,
reload,
updateTopic,
deleteTopic,
addRelation,
removeRelation,
bulkSave,
restoreSnapshot,
};
}