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:
@@ -48,6 +48,15 @@ $history = array_values(array_filter($history, fn($m) => is_array($m)
|
||||
$category = trim((string)($input['category'] ?? '')) ?: null;
|
||||
$language = in_array($input['language'] ?? 'no', ['no', 'en'], true) ? $input['language'] : 'no';
|
||||
|
||||
// Folder scope: limit retrieval to a folder subtree, ACL-checked.
|
||||
$folderScopeRaw = $input['folder_id'] ?? null;
|
||||
$folderScope = null;
|
||||
if ($folderScopeRaw !== null && $folderScopeRaw !== '' && $folderScopeRaw !== 'all') {
|
||||
$folderScope = $folderScopeRaw === 'unassigned' ? 0 : (int)$folderScopeRaw;
|
||||
}
|
||||
$includeSubfolders = !empty($input['include_subfolders']);
|
||||
$includeRelated = !empty($input['include_related']);
|
||||
|
||||
// SSE setup
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache, no-transform');
|
||||
@@ -75,6 +84,37 @@ try {
|
||||
'user_role' => 'owner',
|
||||
];
|
||||
|
||||
// Apply folder scoping via allowed_folder_ids (supported by ClientRagPipeline).
|
||||
if ($folderScope !== null) {
|
||||
if ($folderScope === 0) {
|
||||
// Unassigned only — currently not supported by allowed_folder_ids; pass empty array
|
||||
// which the pipeline treats as "no folders allowed" → falls back to docs with NULL folder_id.
|
||||
$options['allowed_folder_ids'] = [];
|
||||
} else {
|
||||
$ids = [$folderScope];
|
||||
if ($includeSubfolders) {
|
||||
try {
|
||||
$db = dbnToolsDb();
|
||||
$stack = [$folderScope];
|
||||
$guard = 0;
|
||||
while ($stack && $guard++ < 1000) {
|
||||
$batch = $stack;
|
||||
$stack = [];
|
||||
$ph = implode(',', array_fill(0, count($batch), '?'));
|
||||
$st = $db->prepare("SELECT id FROM client_folders WHERE client_id = ? AND parent_id IN ({$ph}) AND deleted_at IS NULL");
|
||||
$st->execute(array_merge([$clientId], $batch));
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$cid = (int)$r['id'];
|
||||
$ids[] = $cid;
|
||||
$stack[] = $cid;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) { /* tolerated */ }
|
||||
}
|
||||
$options['allowed_folder_ids'] = $ids;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $rag->askStreaming(
|
||||
$question,
|
||||
null, // model: let pipeline choose default
|
||||
@@ -97,12 +137,28 @@ try {
|
||||
];
|
||||
}
|
||||
|
||||
// Related documents from FalkorDB graph (co-citation), based on the top source doc.
|
||||
$related = [];
|
||||
if ($includeRelated && $sources && method_exists($rag, 'relatedDocumentsFromGraph')) {
|
||||
$topDoc = (int)($sources[0]['document_id'] ?? 0);
|
||||
if ($topDoc > 0) {
|
||||
try {
|
||||
$related = $rag->relatedDocumentsFromGraph($topDoc, 6);
|
||||
} catch (Throwable $e) { /* tolerated */ }
|
||||
}
|
||||
}
|
||||
|
||||
sseEmit('done', [
|
||||
'ok' => true,
|
||||
'chunks_used' => (int)($result['chunks_used'] ?? count($sources)),
|
||||
'model' => (string)($result['model'] ?? ''),
|
||||
'response_time_ms'=> (int)($result['response_time_ms'] ?? 0),
|
||||
'sources' => $sources,
|
||||
'related_documents' => $related,
|
||||
'scope' => [
|
||||
'folder_id' => $folderScope,
|
||||
'include_subfolders'=> $includeSubfolders,
|
||||
],
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
sseEmit('fail', ['ok' => false, 'message' => $e->getMessage()]);
|
||||
|
||||
Reference in New Issue
Block a user