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>
207 lines
7.9 KiB
PHP
207 lines
7.9 KiB
PHP
<?php
|
|
/**
|
|
* /api/dashboard/document-versions.php — per-document version history.
|
|
*
|
|
* GET ?action=list&document_id=X → { ok, versions: [...] }
|
|
* GET ?action=get&version_id=X → { ok, version: {...with content} }
|
|
* POST ?action=restore body: { document_id, version_id }
|
|
* → { ok, document_id, new_version_number, chunks }
|
|
* POST ?action=delete body: { version_id }
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once dirname(__DIR__, 2) . '/includes/bootstrap.php';
|
|
|
|
dbnToolsRequireAuth();
|
|
|
|
try {
|
|
$tenant = dbnToolsEnsureDashboardTenant();
|
|
} catch (DbnToolsHttpException $e) {
|
|
dbnToolsError($e->getMessage(), $e->status, $e->errorCode);
|
|
}
|
|
$clientId = (int)$tenant['client_id'];
|
|
$userId = (int)($tenant['client_user_id'] ?? 0);
|
|
$tenantRole = (string)($tenant['role'] ?? 'editor');
|
|
|
|
$db = dbnToolsDb();
|
|
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
|
$action = (string)($_GET['action'] ?? ($method === 'POST' ? '' : 'list'));
|
|
|
|
try {
|
|
switch ($action) {
|
|
case 'list':
|
|
dbnToolsRequireMethod('GET');
|
|
listVersions($db, $clientId, $userId, $tenantRole);
|
|
break;
|
|
case 'get':
|
|
dbnToolsRequireMethod('GET');
|
|
getVersion($db, $clientId, $userId, $tenantRole);
|
|
break;
|
|
case 'restore':
|
|
dbnToolsRequireMethod('POST');
|
|
restoreVersion($db, $clientId, $userId, $tenantRole);
|
|
break;
|
|
case 'delete':
|
|
dbnToolsRequireMethod('POST');
|
|
deleteVersion($db, $clientId, $userId, $tenantRole);
|
|
break;
|
|
default:
|
|
dbnToolsError('Unknown action.', 400, 'unknown_action');
|
|
}
|
|
} catch (DbnToolsHttpException $e) {
|
|
dbnToolsError($e->getMessage(), $e->status, $e->errorCode, $e->extra);
|
|
} catch (Throwable $e) {
|
|
error_log('[dbn-dms/versions] ' . $e->getMessage());
|
|
dbnToolsError('Version operation failed.', 500, 'version_op_failed');
|
|
}
|
|
|
|
function assertDocumentReadable(PDO $db, int $clientId, int $userId, string $tenantRole, int $docId): array
|
|
{
|
|
$stmt = $db->prepare('SELECT id, folder_id, title, current_version FROM client_documents WHERE id = ? AND client_id = ?');
|
|
$stmt->execute([$docId, $clientId]);
|
|
$row = $stmt->fetch();
|
|
if (!$row) {
|
|
dbnToolsError('Document not found.', 404, 'not_found');
|
|
}
|
|
$fid = $row['folder_id'] ? (int)$row['folder_id'] : 0;
|
|
if (!dbnDmsUserCanAccessFolder($fid ?: null, 'read', $clientId, $userId, $tenantRole)) {
|
|
dbnToolsError('Forbidden.', 403, 'forbidden');
|
|
}
|
|
return $row;
|
|
}
|
|
|
|
function listVersions(PDO $db, int $clientId, int $userId, string $tenantRole): void
|
|
{
|
|
$docId = (int)($_GET['document_id'] ?? 0);
|
|
if ($docId <= 0) {
|
|
dbnToolsError('document_id is required.', 400, 'missing_document_id');
|
|
}
|
|
assertDocumentReadable($db, $clientId, $userId, $tenantRole, $docId);
|
|
|
|
$stmt = $db->prepare(
|
|
'SELECT v.id, v.version_number, v.title, v.original_filename, v.file_size_bytes,
|
|
v.word_count, v.notes, v.uploaded_by, v.created_at,
|
|
u.email AS uploaded_email, u.full_name AS uploaded_name
|
|
FROM client_document_versions v
|
|
LEFT JOIN client_users u ON u.id = v.uploaded_by
|
|
WHERE v.document_id = ? AND v.client_id = ?
|
|
ORDER BY v.version_number DESC'
|
|
);
|
|
$stmt->execute([$docId, $clientId]);
|
|
dbnToolsRespond(['ok' => true, 'versions' => $stmt->fetchAll()]);
|
|
}
|
|
|
|
function getVersion(PDO $db, int $clientId, int $userId, string $tenantRole): void
|
|
{
|
|
$vid = (int)($_GET['version_id'] ?? 0);
|
|
if ($vid <= 0) {
|
|
dbnToolsError('version_id is required.', 400, 'missing_version_id');
|
|
}
|
|
$stmt = $db->prepare(
|
|
'SELECT * FROM client_document_versions WHERE id = ? AND client_id = ?'
|
|
);
|
|
$stmt->execute([$vid, $clientId]);
|
|
$row = $stmt->fetch();
|
|
if (!$row) {
|
|
dbnToolsError('Version not found.', 404, 'not_found');
|
|
}
|
|
assertDocumentReadable($db, $clientId, $userId, $tenantRole, (int)$row['document_id']);
|
|
dbnToolsRespond(['ok' => true, 'version' => $row]);
|
|
}
|
|
|
|
function restoreVersion(PDO $db, int $clientId, int $userId, string $tenantRole): void
|
|
{
|
|
$input = dbnToolsJsonInput(5_000);
|
|
$docId = (int)($input['document_id'] ?? 0);
|
|
$vid = (int)($input['version_id'] ?? 0);
|
|
if ($docId <= 0 || $vid <= 0) {
|
|
dbnToolsError('document_id and version_id are required.', 400, 'missing_args');
|
|
}
|
|
$cur = assertDocumentReadable($db, $clientId, $userId, $tenantRole, $docId);
|
|
$fid = $cur['folder_id'] ? (int)$cur['folder_id'] : 0;
|
|
if (!dbnDmsUserCanAccessFolder($fid ?: null, 'write', $clientId, $userId, $tenantRole)) {
|
|
dbnToolsError('Forbidden.', 403, 'forbidden');
|
|
}
|
|
|
|
$vstmt = $db->prepare('SELECT * FROM client_document_versions WHERE id = ? AND document_id = ? AND client_id = ?');
|
|
$vstmt->execute([$vid, $docId, $clientId]);
|
|
$ver = $vstmt->fetch();
|
|
if (!$ver) {
|
|
dbnToolsError('Version not found.', 404, 'not_found');
|
|
}
|
|
|
|
// Snapshot current state before restoring so we can roll-forward later.
|
|
dbnDmsSnapshotVersion($docId, $clientId, $userId, "Snapshot before restoring v{$ver['version_number']}");
|
|
$next = (int)$db->query("SELECT current_version FROM client_documents WHERE id = {$docId}")->fetchColumn();
|
|
$next = max($next, (int)$ver['version_number']) + 1;
|
|
|
|
$upd = $db->prepare(
|
|
"UPDATE client_documents
|
|
SET title = ?, content = ?, file_size_bytes = ?, word_count = ?,
|
|
original_filename = ?, storage_path = ?, current_version = ?, status = 'pending',
|
|
error_message = NULL, updated_at = NOW()
|
|
WHERE id = ? AND client_id = ?"
|
|
);
|
|
$upd->execute([
|
|
(string)$ver['title'],
|
|
(string)$ver['content'],
|
|
(int)$ver['file_size_bytes'],
|
|
(int)$ver['word_count'],
|
|
$ver['original_filename'],
|
|
$ver['storage_path'],
|
|
$next,
|
|
$docId, $clientId,
|
|
]);
|
|
|
|
try {
|
|
$db->prepare('DELETE FROM client_chunks WHERE client_id = ? AND document_id = ?')->execute([$clientId, $docId]);
|
|
} catch (Throwable $e) { /* tolerated */ }
|
|
|
|
$chunks = 0;
|
|
try {
|
|
$rag = new ClientRagPipeline($clientId);
|
|
$chunks = (int)$rag->ingestDocument($docId);
|
|
} catch (Throwable $e) {
|
|
$db->prepare("UPDATE client_documents SET status='error', error_message=? WHERE id=?")
|
|
->execute([substr($e->getMessage(), 0, 1000), $docId]);
|
|
}
|
|
|
|
dbnDmsLogAudit($clientId, $userId ?: null, 'restore_version',
|
|
['version_id' => $vid, 'restored_to' => $next], $docId, $fid ?: null);
|
|
|
|
dbnToolsRespond([
|
|
'ok' => true,
|
|
'document_id' => $docId,
|
|
'new_version_number' => $next,
|
|
'chunks' => $chunks,
|
|
]);
|
|
}
|
|
|
|
function deleteVersion(PDO $db, int $clientId, int $userId, string $tenantRole): void
|
|
{
|
|
$input = dbnToolsJsonInput(2_000);
|
|
$vid = (int)($input['version_id'] ?? 0);
|
|
if ($vid <= 0) {
|
|
dbnToolsError('version_id is required.', 400, 'missing_version_id');
|
|
}
|
|
$stmt = $db->prepare('SELECT document_id, storage_path FROM client_document_versions WHERE id = ? AND client_id = ?');
|
|
$stmt->execute([$vid, $clientId]);
|
|
$ver = $stmt->fetch();
|
|
if (!$ver) {
|
|
dbnToolsError('Version not found.', 404, 'not_found');
|
|
}
|
|
$cur = assertDocumentReadable($db, $clientId, $userId, $tenantRole, (int)$ver['document_id']);
|
|
$fid = $cur['folder_id'] ? (int)$cur['folder_id'] : 0;
|
|
if (!dbnDmsUserCanAccessFolder($fid ?: null, 'write', $clientId, $userId, $tenantRole)) {
|
|
dbnToolsError('Forbidden.', 403, 'forbidden');
|
|
}
|
|
$del = $db->prepare('DELETE FROM client_document_versions WHERE id = ? AND client_id = ?');
|
|
$del->execute([$vid, $clientId]);
|
|
if (!empty($ver['storage_path']) && is_file($ver['storage_path'])) {
|
|
@unlink($ver['storage_path']);
|
|
}
|
|
dbnDmsLogAudit($clientId, $userId ?: null, 'delete_version', ['version_id' => $vid], (int)$ver['document_id']);
|
|
dbnToolsRespond(['ok' => true]);
|
|
}
|