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:
1 parent
8c5c36857f
commit
d18c855571
8 files changed
+494
-73
No files matched your search
+46
-2
@@ -246,8 +246,10 @@ app.delete('/api/photos/:id/collections/:colId', (req, res) => {
|
||||
// ── Duplicates ────────────────────────────────────────────────────────────────
|
||||
|
||||
app.post('/api/dedupe/scan', (_req, res) => {
|
||||
try { res.json(scan(db)); }
|
||||
catch (e) { res.status(500).json({ error: e.message }); }
|
||||
try {
|
||||
const { dedupe_threshold } = db.getSettings();
|
||||
res.json(scan(db, dedupe_threshold));
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
app.get('/api/duplicates', (_req, res) => {
|
||||
@@ -274,6 +276,48 @@ app.post('/api/duplicates/dismiss', (req, res) => {
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
// ── Settings ──────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/settings', (_req, res) => res.json(db.getSettings()));
|
||||
|
||||
app.patch('/api/settings', (req, res) => {
|
||||
const allowed = new Set(['thumb_size', 'thumb_quality', 'preview_size', 'preview_quality', 'dedupe_threshold']);
|
||||
for (const [key, value] of Object.entries(req.body)) {
|
||||
if (!allowed.has(key)) continue;
|
||||
const n = parseInt(value, 10);
|
||||
if (!isNaN(n)) db.setSetting(key, n);
|
||||
}
|
||||
res.json(db.getSettings());
|
||||
});
|
||||
|
||||
// ── Watch folders ─────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/watch-folders', (_req, res) => res.json(db.listWatchFolders()));
|
||||
|
||||
app.post('/api/watch-folders', (req, res) => {
|
||||
const p = (req.body.path ?? '').trim();
|
||||
if (!p) return badRequest(res, 'path required');
|
||||
db.addWatchFolder(p);
|
||||
res.status(201).json(db.listWatchFolders());
|
||||
});
|
||||
|
||||
app.delete('/api/watch-folders/:id', (req, res) => {
|
||||
db.removeWatchFolder(parseInt(req.params.id, 10));
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
app.patch('/api/watch-folders/:id', (req, res) => {
|
||||
db.toggleWatchFolder(parseInt(req.params.id, 10), req.body.enabled);
|
||||
res.json(db.listWatchFolders());
|
||||
});
|
||||
|
||||
// ── Admin ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
app.post('/api/admin/clear-dismissals', (_req, res) => {
|
||||
db.raw.prepare('UPDATE duplicate_pairs SET dismissed = 0').run();
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
// ── SPA fallback ──────────────────────────────────────────────────────────────
|
||||
|
||||
const distPath = path.resolve(__dirname, '../dist');
|
||||
|
||||
Reference in new issue
Block a user