diff --git a/pb_migrations/1780000000_created_handbook_sync_state.js b/pb_migrations/1780000000_created_handbook_sync_state.js
new file mode 100644
index 0000000..23f7d0e
--- /dev/null
+++ b/pb_migrations/1780000000_created_handbook_sync_state.js
@@ -0,0 +1,90 @@
+///
+migrate((app) => {
+ const collection = new Collection({
+ "createRule": "",
+ "deleteRule": "",
+ "fields": [
+ {
+ "autogeneratePattern": "[a-z0-9]{15}",
+ "help": "",
+ "hidden": false,
+ "id": "text3208210256",
+ "max": 15,
+ "min": 15,
+ "name": "id",
+ "pattern": "^[a-z0-9]+$",
+ "presentable": false,
+ "primaryKey": true,
+ "required": true,
+ "system": true,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "help": "",
+ "hidden": false,
+ "id": "text_path123",
+ "max": 0,
+ "min": 0,
+ "name": "path",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": true,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "help": "",
+ "hidden": false,
+ "id": "text_sha123",
+ "max": 0,
+ "min": 0,
+ "name": "sha",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": true,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "hidden": false,
+ "id": "autodate2990389176",
+ "name": "created",
+ "onCreate": true,
+ "onUpdate": false,
+ "presentable": false,
+ "system": false,
+ "type": "autodate"
+ },
+ {
+ "hidden": false,
+ "id": "autodate3332085495",
+ "name": "updated",
+ "onCreate": true,
+ "onUpdate": true,
+ "presentable": false,
+ "system": false,
+ "type": "autodate"
+ }
+ ],
+ "id": "pbc_handbooksync123",
+ "indexes": [
+ "CREATE UNIQUE INDEX `idx_handbook_path` ON `handbook_sync_state` (`path`)"
+ ],
+ "listRule": "",
+ "name": "handbook_sync_state",
+ "system": false,
+ "type": "base",
+ "updateRule": "",
+ "viewRule": ""
+ });
+
+ return app.save(collection);
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_handbooksync123");
+
+ return app.delete(collection);
+})
diff --git a/src/components/admin/KnowledgeGraph.jsx b/src/components/admin/KnowledgeGraph.jsx
index 99a3488..cbdc98a 100644
--- a/src/components/admin/KnowledgeGraph.jsx
+++ b/src/components/admin/KnowledgeGraph.jsx
@@ -1,8 +1,9 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
-import { Trash2, Edit2, Save, X, RefreshCw, AlertCircle, Plus, Link as LinkIcon } from 'lucide-react';
+import { Trash2, Edit2, Save, X, RefreshCw, AlertCircle, Plus, Link as LinkIcon, Github } from 'lucide-react';
import * as db from '../../lib/db';
import { anthropicApi } from '../../lib/api';
+import { getRepoFolder } from '../../lib/giteaService';
import Button from '../ui/Button';
import SuggestionsQueue from './SuggestionsQueue';
@@ -17,6 +18,10 @@ const KnowledgeGraph = () => {
const [analyzeError, setAnalyzeError] = useState(null);
const [newRelData, setNewRelData] = useState({ target: '', type: 'related_to' });
+ const [isSyncing, setIsSyncing] = useState(false);
+ const [syncResult, setSyncResult] = useState(null);
+ const [syncError, setSyncError] = useState(null);
+
const [topics, setTopics] = useState([]);
const [relations, setRelations] = useState([]);
@@ -191,6 +196,38 @@ const KnowledgeGraph = () => {
)));
};
+ const syncHandbook = async () => {
+ setIsSyncing(true);
+ setSyncError(null);
+ setSyncResult(null);
+
+ try {
+ const files = await getRepoFolder('respellion', 'employee-handbook', 'docs');
+ const syncStates = await db.getHandbookSyncStates();
+
+ const added = [];
+ const modified = [];
+ const unchanged = [];
+
+ files.forEach(file => {
+ const state = syncStates.find(s => s.path === file.path);
+ if (!state) {
+ added.push(file);
+ } else if (state.sha !== file.sha) {
+ modified.push(file);
+ } else {
+ unchanged.push(file);
+ }
+ });
+
+ setSyncResult({ added, modified, unchanged });
+ } catch (e) {
+ setSyncError(e.message || 'Failed to check GitHub for updates.');
+ } finally {
+ setIsSyncing(false);
+ }
+ };
+
const analyzeGraph = async () => {
if (!confirm('This will send the entire graph to the AI to evaluate content, merge duplicates, and create new logical relations. This may take a moment. Proceed?')) return;
@@ -281,21 +318,52 @@ Analyze this graph and return ONLY the optimized JSON object with this EXACT str
-
-
- {analyzeError && (
-
-
- {analyzeError}
-
- )}
+
+
+
+ {analyzeError && (
+
+
+ {analyzeError}
+
+ )}
+
+
+
+
+ {syncError && (
+
+
+ {syncError}
+
+ )}
+ {syncResult && (
+
+
GitHub Sync Status:
+
Added files: {syncResult.added.length}
+
Modified files: {syncResult.modified.length}
+
Unchanged: {syncResult.unchanged.length}
+ {(syncResult.added.length > 0 || syncResult.modified.length > 0) && (
+
Ready for Phase 2 implementation.
+ )}
+
+ )}
+
{/* R42 chatbot suggestions queue */}
diff --git a/src/lib/db.js b/src/lib/db.js
index 74cc1e5..92ceff6 100644
--- a/src/lib/db.js
+++ b/src/lib/db.js
@@ -308,3 +308,20 @@ export async function bulkSetCurriculum(year, weeks) {
);
}
+// ── Handbook Sync State ───────────────────────────────────────────────────────
+
+export async function getHandbookSyncStates() {
+ try {
+ return await pb.collection('handbook_sync_state').getFullList();
+ } catch { return []; }
+}
+
+export async function updateHandbookSyncState(path, sha) {
+ try {
+ const r = await pb.collection('handbook_sync_state').getFirstListItem(`path="${path}"`);
+ return await pb.collection('handbook_sync_state').update(r.id, { sha });
+ } catch {
+ return await pb.collection('handbook_sync_state').create({ path, sha });
+ }
+}
+