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:
+44
-1
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user