feat: settings panel + multi-folder scanning

Settings panel (⚙ in header):
- Watch Folders: add/remove paths, toggle enabled; worker polls every 30s
  and calls watcher.add/unwatch without restart
- Ingestion: thumb/preview size and quality, applied per-file at ingest time
- Duplicate Detection: configurable Hamming distance threshold
- Danger Zone: clear all dismissed duplicate pairs

Multi-folder scanning:
- watch_folders table replaces single PHOTO_DIR env var
- PHOTO_DIR env var seeds the first entry on startup
- settings table with defaults for all tunable values
- Worker reads settings from DB on each drain cycle so changes are live

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-18 07:58:44 -04:00
1 parent 8c5c36857f
commit d18c855571
8 files changed
+494 -73

No files matched your search

+23 -4
View File
@@ -5,6 +5,7 @@ import Lightbox from './components/Lightbox.jsx';
import FilterSidebar from './components/FilterSidebar.jsx';
import IngestBadge from './components/IngestBadge.jsx';
import DuplicatesView from './components/DuplicatesView.jsx';
import SettingsPanel from './components/SettingsPanel.jsx';
import { lazy, Suspense } from 'react';
const MapView = lazy(() => import('./components/MapView.jsx'));
@@ -24,8 +25,9 @@ export default function App() {
const [filters, setFilters] = useState(EMPTY_FILTERS);
const [stats, setStats] = useState(null);
const [lbPhotos, setLbPhotos] = useState(null);
const [lbIndex, setLbIndex] = useState(null);
const [lbPhotos, setLbPhotos] = useState(null);
const [lbIndex, setLbIndex] = useState(null);
const [showSettings, setShowSettings] = useState(false);
const loadingRef = useRef(false);
const sentinelRef = useRef(null);
@@ -138,9 +140,23 @@ export default function App() {
</Tab>
</nav>
{/* Ingest progress — right side */}
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end' }}>
{/* Right side: ingest badge + gear */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 10 }}>
<IngestBadge onScanComplete={handleScanComplete} />
<button
onClick={() => setShowSettings(s => !s)}
title="Settings"
style={{
background: showSettings ? '#1e2a3a' : 'transparent',
border: 'none', borderRadius: 6,
color: showSettings ? '#7ab8f5' : '#555',
cursor: 'pointer', fontSize: 16,
width: 32, height: 32,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
⚙
</button>
</div>
</header>
@@ -217,6 +233,9 @@ export default function App() {
</div>
</div>
{/* Settings panel */}
{showSettings && <SettingsPanel onClose={() => setShowSettings(false)} />}
{/* Lightbox */}
{lbPhotos !== null && (
<Lightbox