feat: implement snapshot restore functionality and enhance graph state management
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user