diff --git a/Caddyfile b/Caddyfile
index 157cb61..65def0a 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -6,7 +6,7 @@
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
- Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com"
+ Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.github.com https://raw.githubusercontent.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
}
@@ -19,6 +19,7 @@
}
}
+
handle /api/* {
reverse_proxy pocketbase-learning:8090
}
diff --git a/src/components/admin/UploadZone.jsx b/src/components/admin/UploadZone.jsx
index b8abd3e..5c6ed02 100644
--- a/src/components/admin/UploadZone.jsx
+++ b/src/components/admin/UploadZone.jsx
@@ -1,24 +1,46 @@
-import React, { useState, useRef } from 'react';
-import { UploadCloud, Link as LinkIcon, AlertCircle, CheckCircle } from 'lucide-react';
+import React, { useState, useRef, useEffect } from 'react';
+import { UploadCloud, AlertCircle, CheckCircle, RefreshCw, GitBranch, FileText, ArrowUpCircle, MinusCircle } from 'lucide-react';
import { processSourceText } from '../../lib/extractionPipeline';
+import { getRepoFolder, getFileContent, parseGitHubUrl } from '../../lib/giteaService';
+import * as db from '../../lib/db';
import Card from '../ui/Card';
import Button from '../ui/Button';
-import Input from '../ui/Input';
+
+// ── File status badge ─────────────────────────────────────────────────────────
+
+const StatusBadge = ({ status }) => {
+ if (status === 'new') return New;
+ if (status === 'updated') return Updated;
+ if (status === 'current') return Up to date;
+ if (status === 'done') return Processed;
+ if (status === 'error') return Failed;
+ if (status === 'skipped') return Skipped;
+ return null;
+};
+
+// ── Main Component ────────────────────────────────────────────────────────────
const UploadZone = ({ onUploadComplete }) => {
- const [isDragging, setIsDragging] = useState(false);
- const [url, setUrl] = useState('');
- const [isProcessing, setIsProcessing] = useState(false);
- const [status, setStatus] = useState(null); // { type: 'error' | 'success', msg: string }
-
+ const [isDragging, setIsDragging] = useState(false);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [status, setStatus] = useState(null);
+
+ // GitHub sync state
+ const [githubUrl, setGithubUrl] = useState('https://github.com/respellion/employee-handbook/tree/main/docs');
+ const [isFetching, setIsFetching] = useState(false);
+ const [fileList, setFileList] = useState(null);
+ const [syncProgress, setSyncProgress] = useState(null);
+
const fileInputRef = useRef(null);
- const handleDragOver = (e) => {
- e.preventDefault();
- setIsDragging(true);
- };
+ // Load GitHub URL from PocketBase settings on mount
+ useEffect(() => {
+ db.getSetting('github:url', '').then(url => {
+ if (url) setGithubUrl(url);
+ });
+ }, []);
- const handleDragLeave = () => setIsDragging(false);
+ // ── File upload (drag & drop / browse) ────────────────────────────────────
const processFile = async (file) => {
setIsProcessing(true);
@@ -35,6 +57,9 @@ const UploadZone = ({ onUploadComplete }) => {
}
};
+ const handleDragOver = (e) => { e.preventDefault(); setIsDragging(true); };
+ const handleDragLeave = () => setIsDragging(false);
+
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
@@ -52,14 +77,95 @@ const UploadZone = ({ onUploadComplete }) => {
if (e.target.files?.length > 0) processFile(e.target.files[0]);
};
- const handleUrlSubmit = async (e) => {
- e.preventDefault();
- setStatus({ type: 'error', msg: 'URL import is disabled in the browser due to CORS restrictions.' });
+ // ── Gitea Sync ─────────────────────────────────────────────────────────────
+
+ const handleFetchFiles = async () => {
+ const parsed = parseGitHubUrl(githubUrl);
+ if (!parsed) {
+ setStatus({ type: 'error', msg: 'Enter a valid GitHub folder URL (e.g. https://github.com/owner/repo/tree/main/docs).' });
+ return;
+ }
+ setIsFetching(true);
+ setFileList(null);
+ setStatus(null);
+ try {
+ const files = await getRepoFolder(parsed.owner, parsed.repo, parsed.folder, parsed.branch);
+ const enriched = await Promise.all(files.map(async (f) => {
+ const storedSha = await db.getSetting(`github:sha:${f.name}`, null);
+ let syncStatus = 'new';
+ if (storedSha === f.sha) syncStatus = 'current';
+ else if (storedSha && storedSha !== f.sha) syncStatus = 'updated';
+ return { ...f, syncStatus, _parsed: parsed };
+ }));
+ setFileList(enriched);
+ // Persist URL for next visit
+ await db.setSetting('github:url', githubUrl.trim());
+ } catch (err) {
+ setStatus({ type: 'error', msg: `Could not fetch from GitHub: ${err.message}` });
+ } finally {
+ setIsFetching(false);
+ }
};
+ const handleSync = async () => {
+ const toProcess = fileList.filter(f => f.syncStatus === 'new' || f.syncStatus === 'updated');
+ if (toProcess.length === 0) {
+ setStatus({ type: 'success', msg: 'Everything is already up to date.' });
+ return;
+ }
+
+ setIsProcessing(true);
+ setSyncProgress({ current: 0, total: toProcess.length });
+ let processed = 0, failed = 0;
+
+ // Update a single file's status in the list
+ const updateFileStatus = (filename, newStatus) => {
+ setFileList(prev => prev.map(f => f.name === filename ? { ...f, syncStatus: newStatus } : f));
+ };
+
+ for (const file of toProcess) {
+ const { _parsed: p } = file;
+ try {
+ const content = await getFileContent(p.owner, p.repo, file.path, p.branch);
+
+ // If this is an update, delete the old source so deduplication allows re-processing
+ if (file.syncStatus === 'updated') {
+ const existing = await db.getSources();
+ const old = existing.find(s => s.name === file.name && s.status === 'completed');
+ if (old) await db.deleteSource(old.id);
+ }
+
+ await processSourceText(content, file.name);
+ await db.setSetting(`github:sha:${file.name}`, file.sha);
+ updateFileStatus(file.name, 'done');
+ processed++;
+ } catch (err) {
+ console.error(`[GitHub Sync] Failed: ${file.name}`, err);
+ updateFileStatus(file.name, 'error');
+ failed++;
+ }
+ setSyncProgress(p => ({ ...p, current: p.current + 1 }));
+ }
+
+ setSyncProgress(null);
+ setIsProcessing(false);
+ if (onUploadComplete) onUploadComplete();
+
+ const parts = [];
+ if (processed > 0) parts.push(`${processed} file${processed > 1 ? 's' : ''} processed`);
+ if (failed > 0) parts.push(`${failed} failed`);
+ setStatus({ type: failed > 0 ? 'error' : 'success', msg: `Sync complete: ${parts.join(', ')}.` });
+ };
+
+ const toProcess = fileList ? fileList.filter(f => f.syncStatus === 'new' || f.syncStatus === 'updated') : [];
+
+ // ── Render ─────────────────────────────────────────────────────────────────
+
return (
-
{
Drag files here
Supports .txt and .md (max 5MB)
-
+ {/* ─── Divider ─── */}
-