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:
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||
$dashboardPage = 'trash';
|
||||
$dashboardTitle = dbnToolsT('dash_title_trash', dbnToolsCurrentLanguage()) ?: 'Trash';
|
||||
$dashboardLead = dbnToolsT('dash_lead_trash', dbnToolsCurrentLanguage()) ?: 'Items here are auto-deleted after 30 days.';
|
||||
require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||||
?>
|
||||
<section class="dms-shell dms-shell--single">
|
||||
<div class="dms-main">
|
||||
<div class="dms-toolbar">
|
||||
<div class="dms-toolbar__crumbs"><span>🗑 Trash</span></div>
|
||||
<div class="dms-toolbar__actions">
|
||||
<button class="dash-btn" type="button" id="trashRestoreSel" disabled>Restore selected</button>
|
||||
<button class="dash-btn dash-btn--danger" type="button" id="trashPurgeAll">Empty trash (admin)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="trashList"><div class="dms-loading"></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
const DMS = window.DBN_DMS;
|
||||
const $list = document.getElementById('trashList');
|
||||
const $restore = document.getElementById('trashRestoreSel');
|
||||
const $purge = document.getElementById('trashPurgeAll');
|
||||
const selected = new Set();
|
||||
|
||||
async function load() {
|
||||
$list.innerHTML = '<div class="dms-loading"></div>';
|
||||
try {
|
||||
const data = await DMS.api('/trash.php?action=list&limit=200');
|
||||
const items = data.items || [];
|
||||
if (!items.length) {
|
||||
$list.innerHTML = '<div class="dms-list"><div class="dms-list__empty"><strong>Trash is empty</strong><span>Nothing to restore.</span></div></div>';
|
||||
return;
|
||||
}
|
||||
const rows = items.map(it => {
|
||||
const id = (it.kind === 'document' ? 'd' : 'f') + it.id;
|
||||
const expires = (it.expires_in_days ?? 0) + 'd left';
|
||||
return '<div class="dms-list__row" data-key="' + id + '" data-kind="' + it.kind + '" data-id="' + it.id + '">' +
|
||||
'<input type="checkbox" class="dms-trash-sel">' +
|
||||
'<div class="dms-list__title">' +
|
||||
'<span class="dms-list__title-icon">' + (it.kind === 'folder' ? '📁' : DMS.fileIcon(it.source_type)) + '</span>' +
|
||||
DMS.safe(it.title || it.name || '(untitled)') +
|
||||
'</div>' +
|
||||
'<div class="dms-list__cell dms-list__cell--muted">' + DMS.safe(it.kind) + '</div>' +
|
||||
'<div class="dms-list__cell dms-list__cell--muted">' + DMS.fmtRelative(it.deleted_at) + '</div>' +
|
||||
'<div class="dms-list__cell dms-list__cell--muted">' + expires + '</div>' +
|
||||
'<div class="dms-list__cell"></div>' +
|
||||
'<button class="dms-list__more" data-id="' + it.id + '" data-kind="' + it.kind + '" type="button">⋯</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
$list.innerHTML = '<div class="dms-list">' +
|
||||
'<div class="dms-list__head"><span></span><span>Title</span><span>Kind</span><span>Trashed</span><span>Expires</span><span></span><span></span></div>' +
|
||||
rows + '</div>';
|
||||
|
||||
$list.querySelectorAll('.dms-trash-sel').forEach(cb => {
|
||||
cb.addEventListener('change', e => {
|
||||
const row = e.target.closest('.dms-list__row');
|
||||
if (e.target.checked) selected.add(row.dataset.key); else selected.delete(row.dataset.key);
|
||||
$restore.disabled = selected.size === 0;
|
||||
});
|
||||
});
|
||||
$list.querySelectorAll('.dms-list__more').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
const id = Number(btn.dataset.id);
|
||||
const kind = btn.dataset.kind;
|
||||
DMS.openCtxMenu(btn.getBoundingClientRect().left, btn.getBoundingClientRect().bottom, [
|
||||
{ label: 'Restore', icon: '↩', onclick: () => restoreItems([{kind, id}]) },
|
||||
'-',
|
||||
{ label: 'Delete permanently', icon: '🔥', danger: true,
|
||||
onclick: () => purgeItems([{kind, id}]) },
|
||||
]);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
$list.innerHTML = '<div class="dms-list__empty"><strong>Error</strong><span>' + DMS.safe(e.message) + '</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreItems(items) {
|
||||
const body = { document_ids: [], folder_ids: [] };
|
||||
items.forEach(i => {
|
||||
if (i.kind === 'folder') body.folder_ids.push(i.id);
|
||||
else body.document_ids.push(i.id);
|
||||
});
|
||||
try {
|
||||
await DMS.api('/trash.php?action=restore', { method: 'POST', body });
|
||||
selected.clear();
|
||||
$restore.disabled = true;
|
||||
load();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function purgeItems(items) {
|
||||
if (!confirm('Permanently delete? This cannot be undone.')) return;
|
||||
const body = { document_ids: [], folder_ids: [] };
|
||||
items.forEach(i => {
|
||||
if (i.kind === 'folder') body.folder_ids.push(i.id);
|
||||
else body.document_ids.push(i.id);
|
||||
});
|
||||
try {
|
||||
await DMS.api('/trash.php?action=purge', { method: 'POST', body });
|
||||
load();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
$restore.addEventListener('click', () => {
|
||||
const items = Array.from(selected).map(k => ({ kind: k[0] === 'd' ? 'document' : 'folder', id: Number(k.slice(1)) }));
|
||||
restoreItems(items);
|
||||
});
|
||||
$purge.addEventListener('click', async () => {
|
||||
if (!confirm('Permanently delete ALL items in trash? This cannot be undone.')) return;
|
||||
try {
|
||||
await DMS.api('/trash.php?action=purge', { method: 'POST', body: { all: true } });
|
||||
load();
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/layout_dashboard_footer.php'; ?>
|
||||
Reference in New Issue
Block a user