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:
2026-05-26 22:24:56 +02:00
parent b84827ecea
commit 2e2b0b45fa
30 changed files with 5438 additions and 335 deletions
+44 -1
View File
@@ -11,6 +11,22 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
</div>
<section class="dash-card" style="display:flex; flex-direction:column; min-height:60vh;">
<div class="dms-filters" style="margin-bottom:8px;">
<label style="font-size:13px;display:inline-flex;gap:6px;align-items:center;">
📁 <span>Scope:</span>
<select id="chatFolderScope">
<option value="all">All folders (whole tenant)</option>
<option value="unassigned">Unassigned only</option>
</select>
</label>
<label style="font-size:13px;display:inline-flex;align-items:center;gap:4px;">
<input type="checkbox" id="chatIncludeSub" checked> Include subfolders
</label>
<label style="font-size:13px;display:inline-flex;align-items:center;gap:4px;">
<input type="checkbox" id="chatIncludeRelated" checked> Show related authorities (graph)
</label>
</div>
<div id="chatLog" class="chat-log" aria-live="polite">
<div class="chat-empty" id="chatEmptyMsg"></div>
</div>
@@ -113,10 +129,19 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
let answer = '';
try {
const folderScope = document.getElementById('chatFolderScope');
const includeSub = document.getElementById('chatIncludeSub');
const includeRel = document.getElementById('chatIncludeRelated');
const body = { question, history: history.slice(0, -1) };
if (folderScope && folderScope.value && folderScope.value !== 'all') {
body.folder_id = folderScope.value;
body.include_subfolders = includeSub && includeSub.checked;
}
if (includeRel && includeRel.checked) body.include_related = true;
const resp = await fetch(api + '/chat-stream.php', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question, history: history.slice(0, -1) }),
body: JSON.stringify(body),
});
if (!resp.ok || !resp.body) throw new Error('HTTP ' + resp.status);
@@ -152,6 +177,7 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
} else if (evName === 'done') {
history.push({ role: 'assistant', content: answer });
renderSources(sources, payload.sources || []);
renderRelated(aiNode, payload.related_documents || []);
const chunksTmpl = (I18N.chat_passages_meta || '{n} passages').replace('{n}', payload.chunks_used || 0);
meta.hidden = false;
meta.textContent = chunksTmpl + ' · ' + (payload.model || 'auto') + ' · ' + (payload.response_time_ms || 0) + ' ms';
@@ -190,6 +216,23 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
}).join('');
}
function renderRelated(node, related) {
if (!related || !related.length) return;
let rel = node.querySelector('.chat-related');
if (!rel) {
rel = document.createElement('div');
rel.className = 'chat-related';
rel.style.cssText = 'display:flex;flex-wrap:wrap;gap:0.35rem;margin-top:0.4rem;';
const sources = node.querySelector('.chat-sources');
sources.parentNode.insertBefore(rel, sources.nextSibling);
}
const label = '<small style="opacity:.6;width:100%;display:block;">↳ Related authorities (graph):</small>';
rel.innerHTML = label + related.slice(0, 6).map(r =>
'<a class="chat-source-chip" href="/dashboard/document.php?id=' + r.doc_id + '" style="background:rgba(184,138,44,0.16);color:#6c5212;border-color:rgba(184,138,44,0.4)">'
+ safe(r.title || ('doc #' + r.doc_id)) + ' · ' + safe(r.shared) + '⋆</a>'
).join(' ');
}
function wireActions(node, question, answer) {
node.querySelector('.chat-copy').addEventListener('click', () => {
navigator.clipboard.writeText(answer).then(() => {