2e2b0b45fa
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>
165 lines
8.6 KiB
PHP
165 lines
8.6 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||
$dashboardPage = 'folders';
|
||
$dashboardTitle = dbnToolsT('dash_title_folders', dbnToolsCurrentLanguage()) ?: 'Folders';
|
||
$dashboardLead = dbnToolsT('dash_lead_folders', dbnToolsCurrentLanguage()) ?: 'Organise documents and set per-folder access.';
|
||
require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||
?>
|
||
<section class="dms-shell">
|
||
<aside class="dms-tree" id="dmsTree" aria-label="Folder tree">
|
||
<div class="dms-loading"></div>
|
||
</aside>
|
||
<div class="dms-main">
|
||
<div class="dms-toolbar">
|
||
<div class="dms-toolbar__crumbs" id="folderCrumbs"><span>Pick a folder on the left</span></div>
|
||
<div class="dms-toolbar__actions">
|
||
<button class="dash-btn dash-btn--primary" id="folderNewBtn" type="button">+ New folder</button>
|
||
</div>
|
||
</div>
|
||
<div id="folderDetail">
|
||
<div class="dms-list__empty">
|
||
<strong>Select a folder</strong>
|
||
<span>Use the tree on the left, or create a new folder.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
const DMS = window.DBN_DMS;
|
||
const $tree = document.getElementById('dmsTree');
|
||
const $detail = document.getElementById('folderDetail');
|
||
const $crumbs = document.getElementById('folderCrumbs');
|
||
let selectedFolderId = null;
|
||
|
||
async function loadTree() {
|
||
await DMS.Tree.load();
|
||
DMS.Tree.render($tree, { activeFolderId: selectedFolderId });
|
||
}
|
||
|
||
async function showDetail(folderId) {
|
||
if (folderId === 'all' || folderId === 'unassigned' || folderId === 'trash') {
|
||
$detail.innerHTML = '<div class="dms-list__empty"><strong>System folder</strong><span>Cannot configure access for system folders.</span></div>';
|
||
return;
|
||
}
|
||
selectedFolderId = folderId;
|
||
$detail.innerHTML = '<div class="dms-loading"></div>';
|
||
try {
|
||
const [perms, crumbsData] = await Promise.all([
|
||
DMS.api('/folders.php?action=list_permissions&folder_id=' + folderId),
|
||
DMS.api('/folders.php?action=get_breadcrumb&folder_id=' + folderId),
|
||
]);
|
||
$crumbs.innerHTML = (crumbsData.breadcrumb || []).map((b, i, a) =>
|
||
(i ? '<span class="dms-toolbar__crumb-sep">›</span>' : '')
|
||
+ (i === a.length - 1
|
||
? '<span class="dms-toolbar__crumb--current">' + DMS.safe(b.name) + '</span>'
|
||
: '<span>' + DMS.safe(b.name) + '</span>')
|
||
).join('');
|
||
|
||
const rows = (perms.permissions || []).map(p => {
|
||
const who = p.user_id
|
||
? '👤 ' + DMS.safe(p.user_email || ('user #' + p.user_id))
|
||
: '🛡 role ≥ ' + DMS.safe(p.min_role);
|
||
return '<div class="dms-perm-row">' +
|
||
'<div>' + who + '</div>' +
|
||
'<div>' + (p.can_read ? '✓' : '—') + ' read</div>' +
|
||
'<div>' + (p.can_write ? '✓' : '—') + ' write</div>' +
|
||
'<div>' + (p.can_manage ? '✓' : '—') + ' manage</div>' +
|
||
'<div><button class="dms-list__more" data-pid="' + p.id + '" type="button" aria-label="Remove">✕</button></div>' +
|
||
'</div>';
|
||
}).join('');
|
||
|
||
$detail.innerHTML =
|
||
'<div class="dms-toolbar"><div class="dms-toolbar__crumbs"><strong>Access control</strong></div>' +
|
||
'<div class="dms-toolbar__actions">' +
|
||
'<button class="dash-btn" type="button" id="folderRename">Rename</button>' +
|
||
'<button class="dash-btn dash-btn--danger" type="button" id="folderDelete">Delete</button>' +
|
||
'<button class="dash-btn dash-btn--primary" type="button" id="folderAddAcl">+ Add rule</button>' +
|
||
'</div></div>' +
|
||
'<div class="dms-list">' +
|
||
(rows ||
|
||
'<div class="dms-list__empty"><strong>Open to all in tenant</strong>' +
|
||
'<span>No rules set — add one to restrict access.</span></div>') +
|
||
(rows ? '' : '') +
|
||
'</div>';
|
||
|
||
$detail.querySelector('#folderRename').addEventListener('click', () => {
|
||
DMS.folderModal.open({ folderId, name: crumbsData.breadcrumb.slice(-1)[0]?.name || '' });
|
||
});
|
||
$detail.querySelector('#folderDelete').addEventListener('click', async () => {
|
||
if (!confirm('Soft-delete this folder? Documents inside will move to Trash.')) return;
|
||
try {
|
||
await DMS.api('/folders.php?action=delete', { method: 'POST', body: { folder_id: folderId } });
|
||
selectedFolderId = null;
|
||
$detail.innerHTML = '<div class="dms-list__empty"><strong>Deleted</strong></div>';
|
||
await loadTree();
|
||
} catch (e) { alert(e.message); }
|
||
});
|
||
$detail.querySelector('#folderAddAcl').addEventListener('click', () => openAclModal(folderId));
|
||
$detail.querySelectorAll('[data-pid]').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
if (!confirm('Remove this access rule?')) return;
|
||
try {
|
||
await DMS.api('/folders.php?action=remove_permission', { method: 'POST', body: { permission_id: Number(btn.dataset.pid) } });
|
||
showDetail(folderId);
|
||
} catch (e) { alert(e.message); }
|
||
});
|
||
});
|
||
} catch (e) {
|
||
$detail.innerHTML = '<div class="dms-list__empty"><strong>Error</strong><span>' + DMS.safe(e.message) + '</span></div>';
|
||
}
|
||
}
|
||
|
||
function openAclModal(folderId) {
|
||
DMS.openModal({
|
||
title: 'Add access rule',
|
||
body:
|
||
'<div class="dms-field"><label>Grantee type</label>' +
|
||
'<select id="aclType"><option value="role">Role-based</option><option value="user">Specific user (by id)</option></select></div>' +
|
||
'<div class="dms-field" id="aclRoleField"><label>Minimum role</label>' +
|
||
'<select id="aclRole"><option>viewer</option><option>editor</option><option>admin</option><option>owner</option></select></div>' +
|
||
'<div class="dms-field" id="aclUserField" style="display:none"><label>User ID</label><input type="number" id="aclUser"></div>' +
|
||
'<div class="dms-field dms-field--inline"><label>Permissions</label>' +
|
||
'<label><input type="checkbox" id="aclRead" checked> read</label>' +
|
||
'<label><input type="checkbox" id="aclWrite"> write</label>' +
|
||
'<label><input type="checkbox" id="aclManage"> manage</label></div>',
|
||
actions: [
|
||
{ label: 'Cancel', cls: 'dash-btn', onclick: DMS.closeModal },
|
||
{ label: 'Add', cls: 'dash-btn dash-btn--primary', onclick: async () => {
|
||
const type = document.getElementById('aclType').value;
|
||
const body = { folder_id: folderId,
|
||
can_read: document.getElementById('aclRead').checked,
|
||
can_write: document.getElementById('aclWrite').checked,
|
||
can_manage: document.getElementById('aclManage').checked,
|
||
};
|
||
if (type === 'role') body.min_role = document.getElementById('aclRole').value;
|
||
else body.user_id = Number(document.getElementById('aclUser').value || 0);
|
||
try {
|
||
await DMS.api('/folders.php?action=set_permission', { method: 'POST', body });
|
||
DMS.closeModal();
|
||
showDetail(folderId);
|
||
} catch (e) { alert(e.message); }
|
||
}},
|
||
],
|
||
});
|
||
document.getElementById('aclType').addEventListener('change', e => {
|
||
document.getElementById('aclRoleField').style.display = e.target.value === 'role' ? '' : 'none';
|
||
document.getElementById('aclUserField').style.display = e.target.value === 'user' ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
document.addEventListener('dms:folder-changed', e => showDetail(e.detail.folderId));
|
||
document.addEventListener('dms:reload-required', loadTree);
|
||
document.addEventListener('dms:reload-tree', loadTree);
|
||
|
||
document.getElementById('folderNewBtn').addEventListener('click', () => DMS.folderModal.open({}));
|
||
|
||
loadTree();
|
||
})();
|
||
</script>
|
||
|
||
<?php require_once __DIR__ . '/../includes/layout_dashboard_footer.php'; ?>
|