Files
dobetternorge-tools/dashboard/trash.php
T
daveadmin 2e2b0b45fa 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>
2026-05-26 22:24:56 +02:00

130 lines
6.0 KiB
PHP

<?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'; ?>