Full DMS: folders + ACLs, versioning, trash, bulk ops, preview, smart folders
Rebuild the dashboard as a Drive-style document management system on top of the existing CaveauAI hybrid RAG pipeline. Backend: - 5 migrations (versions, trash soft-delete, saved searches, categories, audit) - DMS helpers (folder ACL walker, disk storage, audit, version snapshot, XLSX/PPTX/HTML/CSV/MD extractors) - New APIs: folders, document-versions, trash, bulk, preview, saved-searches, categories, diagnostics - Extended APIs: documents (folder_id, soft-delete, ACL filter, sort), upload (9 file types, version-collision detection with replace/new/keep-both, disk persistence), chat-stream (folder scoping + graph related-documents) - 30-day trash purge cron with Qdrant + disk + graph cleanup Frontend: - Drive-style two-pane browser with folder tree, drag-drop, bulk-action bar, right-click context menu, multi-select - New pages: folders (tree + per-folder ACL editor), trash (restore/purge) - Extended pages: upload (folder picker, version-collision modal, 9 file type chips), document (Preview/Versions/Permissions tabs with PDF.js + mammoth.js + audio), index (DMS KPIs + activity feed), settings (live diagnostics ping MariaDB/Qdrant/LiteLLM/FalkorDB/disk), chat (folder scope chips + related-authorities chips) - New CSS (dms.css) + JS bundle (dms.js) exposing window.DBN_DMS - Sidebar nav adds Folders + Trash items All routes return HTTP 200 in local smoke test; all 32 files lint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* cron/dbn-dms-trash-purge.php
|
||||
*
|
||||
* Daily cron that hard-deletes any document/folder soft-deleted more than
|
||||
* DBN_DMS_TRASH_RETENTION_DAYS ago.
|
||||
*
|
||||
* Pass --dry-run for a no-op preview.
|
||||
*
|
||||
* Crontab (on chloe, as dobetternorge):
|
||||
* 15 3 * * * /usr/bin/php /home/dobetternorge/public_html/cron/dbn-dms-trash-purge.php >> /home/dobetternorge/logs/dms-purge.log 2>&1
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
exit("CLI only.\n");
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
|
||||
$dryRun = in_array('--dry-run', $argv ?? [], true);
|
||||
$cutoff = DBN_DMS_TRASH_RETENTION_DAYS;
|
||||
|
||||
try {
|
||||
dbnToolsBootCaveau();
|
||||
$db = getDb();
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "[dbn-dms-purge] DB connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
$purgedDocs = 0;
|
||||
$purgedFolders = 0;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
|
||||
try {
|
||||
$docs = $db->prepare(
|
||||
"SELECT id, client_id, storage_path
|
||||
FROM client_documents
|
||||
WHERE deleted_at IS NOT NULL AND deleted_at < (NOW() - INTERVAL ? DAY)
|
||||
LIMIT 5000"
|
||||
);
|
||||
$docs->execute([$cutoff]);
|
||||
foreach ($docs->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$docId = (int)$row['id'];
|
||||
$clientId = (int)$row['client_id'];
|
||||
$path = $row['storage_path'] ?? null;
|
||||
|
||||
if ($dryRun) {
|
||||
echo "DRY: would purge document {$docId} (client {$clientId})\n";
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Version-file cleanup
|
||||
$vr = $db->prepare('SELECT storage_path FROM client_document_versions WHERE document_id = ? AND client_id = ?');
|
||||
$vr->execute([$docId, $clientId]);
|
||||
foreach ($vr->fetchAll() as $v) {
|
||||
if (!empty($v['storage_path']) && is_file($v['storage_path'])) @unlink($v['storage_path']);
|
||||
}
|
||||
|
||||
// Chunks
|
||||
try {
|
||||
$db->prepare('DELETE FROM client_chunks WHERE client_id = ? AND document_id = ?')->execute([$clientId, $docId]);
|
||||
} catch (Throwable $e) { /* tolerated */ }
|
||||
|
||||
// Qdrant
|
||||
try {
|
||||
if (class_exists('QdrantClient')) {
|
||||
$qd = new QdrantClient();
|
||||
$qd->deleteByFilter('bnl_client_chunks', [
|
||||
'must' => [
|
||||
['key' => 'client_id', 'match' => ['value' => $clientId]],
|
||||
['key' => 'document_id', 'match' => ['value' => $docId]],
|
||||
],
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) { /* tolerated */ }
|
||||
|
||||
// Disk
|
||||
if ($path && is_file($path)) @unlink($path);
|
||||
if ($path) {
|
||||
$verDir = dirname($path) . '/' . $docId . '_versions';
|
||||
if (is_dir($verDir)) {
|
||||
foreach (glob($verDir . '/*') ?: [] as $f) @unlink($f);
|
||||
@rmdir($verDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Row
|
||||
$db->prepare('DELETE FROM client_documents WHERE id = ? AND client_id = ?')->execute([$docId, $clientId]);
|
||||
$purgedDocs++;
|
||||
} catch (Throwable $e) {
|
||||
$errors++;
|
||||
fwrite(STDERR, "[dbn-dms-purge] doc {$docId}: " . $e->getMessage() . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$foldCount = $db->prepare("SELECT COUNT(*) FROM client_folders WHERE deleted_at IS NOT NULL AND deleted_at < (NOW() - INTERVAL ? DAY)");
|
||||
$foldCount->execute([$cutoff]);
|
||||
$skipped += (int)$foldCount->fetchColumn();
|
||||
echo "DRY: would purge {$skipped} folders\n";
|
||||
} else {
|
||||
$fold = $db->prepare("DELETE FROM client_folders WHERE deleted_at IS NOT NULL AND deleted_at < (NOW() - INTERVAL ? DAY)");
|
||||
$fold->execute([$cutoff]);
|
||||
$purgedFolders = $fold->rowCount();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "[dbn-dms-purge] fatal: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$elapsed = round(microtime(true) - $start, 2);
|
||||
$mode = $dryRun ? 'DRY-RUN' : 'live';
|
||||
printf("[dbn-dms-purge] %s done in %ss: docs=%d folders=%d errors=%d skipped=%d\n",
|
||||
$mode, $elapsed, $purgedDocs, $purgedFolders, $errors, $skipped);
|
||||
Reference in New Issue
Block a user