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:
+52
-5
@@ -18,10 +18,19 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||||
|
||||
<form id="upFileForm" class="upload-form" enctype="multipart/form-data" style="display:grid; gap:0.85rem;">
|
||||
<label class="upload-drop" id="upDrop">
|
||||
<input type="file" name="file" id="upFile" accept=".pdf,.docx,.txt" hidden>
|
||||
<input type="file" name="file" id="upFile" accept=".pdf,.docx,.txt,.md,.html,.htm,.csv,.xlsx,.pptx,.json" hidden>
|
||||
<span class="upload-drop__icon">📥</span>
|
||||
<strong id="upDropHint"><?= htmlspecialchars(dbnToolsT('dash_upload_drop_strong', $uiLang)) ?></strong>
|
||||
<small><?= htmlspecialchars(dbnToolsT('dash_upload_drop_small', $uiLang)) ?></small>
|
||||
<div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap;">
|
||||
<span class="dms-chip">PDF</span><span class="dms-chip">DOCX</span><span class="dms-chip">TXT</span>
|
||||
<span class="dms-chip">MD</span><span class="dms-chip">HTML</span><span class="dms-chip">CSV</span>
|
||||
<span class="dms-chip">XLSX</span><span class="dms-chip">PPTX</span><span class="dms-chip">JSON</span>
|
||||
</div>
|
||||
</label>
|
||||
<label style="display:grid;gap:4px;font-size:13px;">
|
||||
<span>📁 Destination folder</span>
|
||||
<select name="folder_id" id="upFolderSel"><option value="">(Unassigned)</option></select>
|
||||
</label>
|
||||
<div class="upload-meta">
|
||||
<label><?= htmlspecialchars(dbnToolsT('dash_upload_title_lbl', $uiLang)) ?><input name="title" placeholder="<?= htmlspecialchars(dbnToolsT('dash_upload_title_ph', $uiLang)) ?>"></label>
|
||||
@@ -139,13 +148,51 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||||
|
||||
function safe(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c])); }
|
||||
|
||||
forms.file.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
// Populate folder picker from /api/dashboard/folders.php
|
||||
(async function loadFolders() {
|
||||
try {
|
||||
const data = await fetch(api + '/folders.php?action=list_tree', { credentials: 'same-origin' }).then(r => r.json());
|
||||
const sel = document.getElementById('upFolderSel');
|
||||
if (!sel || !data.tree) return;
|
||||
const flat = [];
|
||||
const walk = (nodes, prefix) => {
|
||||
nodes.forEach(n => {
|
||||
flat.push({ id: n.id, label: prefix + n.name });
|
||||
if (n.children && n.children.length) walk(n.children, prefix + n.name + ' / ');
|
||||
});
|
||||
};
|
||||
walk(data.tree || [], '');
|
||||
const opts = ['<option value="">(Unassigned)</option>'].concat(
|
||||
flat.map(f => '<option value="' + f.id + '">' + safe(f.label) + '</option>')
|
||||
);
|
||||
sel.innerHTML = opts.join('');
|
||||
// Preselect from ?folder=N
|
||||
const initial = new URLSearchParams(location.search).get('folder');
|
||||
if (initial) sel.value = initial;
|
||||
} catch (_) { /* ignored */ }
|
||||
})();
|
||||
|
||||
async function postFile(versionAction) {
|
||||
const fd = new FormData(forms.file);
|
||||
if (versionAction) fd.set('version_action', versionAction);
|
||||
const res = await fetch(api + '/upload.php', { method: 'POST', credentials: 'same-origin', body: fd });
|
||||
const json = await res.json();
|
||||
if (res.status === 409 && json && json.collision) {
|
||||
const action = await (window.DBN_DMS ? DBN_DMS.chooseCollisionAction(fileInput.files[0]?.name || '') : null);
|
||||
if (action) return postFile(action);
|
||||
setStatus('Cancelled.', 'err'); return null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
forms.file.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!fileInput.files.length) { setStatus(I18N.upload_select_file || 'Select a file first.', 'err'); return; }
|
||||
setStatus(I18N.upload_indexing || 'Uploading and indexing…');
|
||||
fetch(api + '/upload.php', { method: 'POST', credentials: 'same-origin', body: fd })
|
||||
.then(r => r.json()).then(handleResult).catch(err => setStatus('❌ ' + safe(err.message), 'err'));
|
||||
try {
|
||||
const data = await postFile();
|
||||
if (data) handleResult(data);
|
||||
} catch (err) { setStatus('❌ ' + safe(err.message), 'err'); }
|
||||
});
|
||||
|
||||
forms.text.addEventListener('submit', (e) => {
|
||||
|
||||
Reference in New Issue
Block a user