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:
@@ -40,6 +40,13 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dms-kpis" id="dmsExtraKpis" aria-label="DMS overview">
|
||||
<div class="dms-kpi"><p class="dms-kpi__label">Storage used</p><p class="dms-kpi__value" id="dmsStorage">—</p><p class="dms-kpi__hint">across all documents</p></div>
|
||||
<div class="dms-kpi"><p class="dms-kpi__label">Folders</p><p class="dms-kpi__value" id="dmsFolders">—</p><p class="dms-kpi__hint">organising your library</p></div>
|
||||
<div class="dms-kpi"><p class="dms-kpi__label">In trash</p><p class="dms-kpi__value" id="dmsTrash">—</p><p class="dms-kpi__hint">auto-purges after 30d</p></div>
|
||||
<div class="dms-kpi"><p class="dms-kpi__label">Smart folders</p><p class="dms-kpi__value" id="dmsSmart">—</p><p class="dms-kpi__hint">saved views</p></div>
|
||||
</section>
|
||||
|
||||
<section class="dash-card">
|
||||
<div class="dash-card__head">
|
||||
<h2 id="recentTitle"></h2>
|
||||
@@ -50,6 +57,11 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||||
<div id="dashRecent" class="dash-loading"></div>
|
||||
</section>
|
||||
|
||||
<section class="dash-card">
|
||||
<div class="dash-card__head"><h2>Recent activity</h2></div>
|
||||
<div id="dmsActivity" class="dms-activity"><div class="dms-loading"></div></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
@@ -148,6 +160,54 @@ require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||||
recent.className = 'dash-error';
|
||||
recent.textContent = (I18N.error_loading || 'Could not load: ') + err.message;
|
||||
});
|
||||
|
||||
// DMS overview tiles
|
||||
(async function loadDmsKpis() {
|
||||
try {
|
||||
const tree = await fetch(api + '/folders.php?action=list_tree', { credentials: 'same-origin' }).then(r => r.json());
|
||||
const allDocs = await fetch(api + '/documents.php?action=list&limit=500', { credentials: 'same-origin' }).then(r => r.json());
|
||||
const storage = (allDocs.documents || []).reduce((s, d) => s + (d.file_size_bytes || 0), 0);
|
||||
document.getElementById('dmsStorage').textContent =
|
||||
storage < 1024*1024 ? Math.round(storage/1024) + ' KB'
|
||||
: storage < 1024*1024*1024 ? (storage/1024/1024).toFixed(1) + ' MB'
|
||||
: (storage/1024/1024/1024).toFixed(2) + ' GB';
|
||||
const countFolders = (nodes) => (nodes || []).reduce((n, x) => n + 1 + countFolders(x.children), 0);
|
||||
document.getElementById('dmsFolders').textContent = countFolders(tree.tree || []);
|
||||
document.getElementById('dmsTrash').textContent = tree.trash_count || 0;
|
||||
} catch (_) { /* ignored */ }
|
||||
try {
|
||||
const ss = await fetch(api + '/saved-searches.php?action=list', { credentials: 'same-origin' }).then(r => r.json());
|
||||
document.getElementById('dmsSmart').textContent = (ss.items || []).length;
|
||||
} catch (_) { /* ignored */ }
|
||||
})();
|
||||
|
||||
// Activity feed — gracefully degrades if endpoint absent
|
||||
(async function loadActivity() {
|
||||
const wrap = document.getElementById('dmsActivity');
|
||||
try {
|
||||
// We don't have a dedicated activity endpoint; fall back to /documents?action=list sorted by updated.
|
||||
const data = await fetch(api + '/documents.php?action=list&limit=15&sort=updated_at&dir=desc', { credentials: 'same-origin' }).then(r => r.json());
|
||||
const items = data.documents || [];
|
||||
if (!items.length) {
|
||||
wrap.innerHTML = '<div class="dms-list__empty"><strong>No activity yet</strong><span>Upload a document to get started.</span></div>';
|
||||
return;
|
||||
}
|
||||
const safe = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c]));
|
||||
const rel = s => { if (!s) return '—'; const d = new Date(s.replace(' ', 'T') + 'Z'); const diff = (Date.now()-d)/1000;
|
||||
if (diff<60) return 'just now'; if (diff<3600) return Math.floor(diff/60)+'m'; if (diff<86400) return Math.floor(diff/3600)+'h';
|
||||
if (diff<86400*7) return Math.floor(diff/86400)+'d'; return d.toLocaleDateString(loc); };
|
||||
const iconForStatus = { ready:'✓', pending:'⏳', processing:'⚙', error:'⚠' };
|
||||
wrap.innerHTML = items.slice(0, 15).map(d =>
|
||||
'<div class="dms-activity__row">' +
|
||||
'<span class="dms-activity__icon">' + (iconForStatus[d.status] || '📄') + '</span>' +
|
||||
'<div><a href="/dashboard/document.php?id=' + d.id + '">' + safe(d.title) + '</a>' +
|
||||
(d.category ? ' <span class="dms-chip dms-chip--cat">' + safe(d.category) + '</span>' : '') + '</div>' +
|
||||
'<span class="dms-activity__time">' + rel(d.updated_at || d.created_at) + '</span></div>'
|
||||
).join('');
|
||||
} catch (e) {
|
||||
wrap.innerHTML = '<div class="dms-list__empty"><strong>Could not load</strong><span>' + e.message + '</span></div>';
|
||||
}
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user