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,165 @@
|
||||
<?php
|
||||
/**
|
||||
* /api/dashboard/diagnostics.php
|
||||
*
|
||||
* Returns live status of the DMS stack for the current tenant:
|
||||
* - MariaDB tenant tables (counts, FULLTEXT index)
|
||||
* - Qdrant bnl_client_chunks collection (points)
|
||||
* - LiteLLM embed endpoint (reachability + model)
|
||||
* - FalkorDB dbn_client_graph (node count for this client)
|
||||
* - On-disk storage usage
|
||||
*
|
||||
* GET → { ok, sections: [{key,label,value,status,detail}] }
|
||||
*/
|
||||
|
||||
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'];
|
||||
|
||||
$out = [];
|
||||
|
||||
/* MariaDB doc/chunk counts */
|
||||
try {
|
||||
$db = dbnToolsDb();
|
||||
$docs = (int)$db->query("SELECT COUNT(*) FROM client_documents WHERE client_id = {$clientId} AND deleted_at IS NULL")->fetchColumn();
|
||||
$deleted = (int)$db->query("SELECT COUNT(*) FROM client_documents WHERE client_id = {$clientId} AND deleted_at IS NOT NULL")->fetchColumn();
|
||||
$chunks = 0;
|
||||
try {
|
||||
$chunks = (int)$db->query("SELECT COUNT(*) FROM client_chunks WHERE client_id = {$clientId}")->fetchColumn();
|
||||
} catch (Throwable $_) {}
|
||||
$out[] = ['key' => 'mariadb', 'label' => 'MariaDB (bnl_admin)',
|
||||
'value' => "{$docs} docs · {$chunks} chunks · {$deleted} trashed",
|
||||
'status' => 'ok',
|
||||
'detail' => 'chloe MySQL — client_documents + client_chunks tables'];
|
||||
} catch (Throwable $e) {
|
||||
$out[] = ['key' => 'mariadb', 'label' => 'MariaDB', 'value' => 'unreachable', 'status' => 'err', 'detail' => $e->getMessage()];
|
||||
}
|
||||
|
||||
/* FULLTEXT index presence */
|
||||
try {
|
||||
$db = dbnToolsDb();
|
||||
$ft = $db->query("SHOW INDEX FROM client_chunks WHERE Key_name = 'ft_content'")->fetchAll();
|
||||
$out[] = ['key' => 'fulltext', 'label' => 'MariaDB FULLTEXT (BM25)',
|
||||
'value' => $ft ? 'present on client_chunks.content' : 'missing',
|
||||
'status' => $ft ? 'ok' : 'warn',
|
||||
'detail' => 'Required for hybrid keyword search; migration 007'];
|
||||
} catch (Throwable $e) {
|
||||
$out[] = ['key' => 'fulltext', 'label' => 'MariaDB FULLTEXT', 'value' => '—', 'status' => 'warn', 'detail' => $e->getMessage()];
|
||||
}
|
||||
|
||||
/* Qdrant — count vectors for this client via scroll */
|
||||
try {
|
||||
$ch = curl_init('http://10.0.1.10:6333/collections/bnl_client_chunks/points/count');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 4,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => json_encode([
|
||||
'exact' => true,
|
||||
'filter' => [ 'must' => [ ['key' => 'client_id', 'match' => ['value' => $clientId]] ] ],
|
||||
]),
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($http === 200 && $body) {
|
||||
$j = json_decode($body, true);
|
||||
$cnt = (int)($j['result']['count'] ?? 0);
|
||||
$out[] = ['key' => 'qdrant', 'label' => 'Qdrant (bnl_client_chunks)',
|
||||
'value' => $cnt . ' points',
|
||||
'status' => 'ok',
|
||||
'detail' => 'Colin Docker @ 10.0.2.10:6333, filtered by client_id'];
|
||||
} else {
|
||||
$out[] = ['key' => 'qdrant', 'label' => 'Qdrant', 'value' => 'http ' . $http, 'status' => 'warn', 'detail' => 'Not reachable from web container; vector search may fall back to MariaDB.'];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$out[] = ['key' => 'qdrant', 'label' => 'Qdrant', 'value' => 'error', 'status' => 'err', 'detail' => $e->getMessage()];
|
||||
}
|
||||
|
||||
/* LiteLLM embed endpoint */
|
||||
try {
|
||||
$ch = curl_init('http://10.0.1.10:4000/v1/models');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 4,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer sk-bnl-litellm-26xR9mK4qvN3wL8sTj7pB2d'],
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$hasEmbed = $body && strpos($body, 'nomic-embed') !== false;
|
||||
$out[] = ['key' => 'litellm', 'label' => 'LiteLLM (Colin)',
|
||||
'value' => $http === 200 ? ($hasEmbed ? 'reachable · nomic-embed-text registered' : 'reachable · embed model not found') : ('http ' . $http),
|
||||
'status' => $http === 200 ? ($hasEmbed ? 'ok' : 'warn') : 'warn',
|
||||
'detail' => 'http://10.0.1.10:4000 — chunker uses /v1/embeddings'];
|
||||
} catch (Throwable $e) {
|
||||
$out[] = ['key' => 'litellm', 'label' => 'LiteLLM', 'value' => 'error', 'status' => 'err', 'detail' => $e->getMessage()];
|
||||
}
|
||||
|
||||
/* FalkorDB nodes for this client */
|
||||
try {
|
||||
$sock = @stream_socket_client('tcp://10.0.2.10:6379', $errno, $errstr, 2);
|
||||
if ($sock) {
|
||||
$cmd = "GRAPH.QUERY";
|
||||
$graph = "dbn_client_graph";
|
||||
$cypher = "MATCH (d:Document {client_id: {$clientId}}) RETURN count(d)";
|
||||
$payload = "*4\r\n$" . strlen($cmd) . "\r\n{$cmd}\r\n$" . strlen($graph) . "\r\n{$graph}\r\n$" . strlen($cypher) . "\r\n{$cypher}\r\n$8\r\n--compact\r\n";
|
||||
fwrite($sock, $payload);
|
||||
stream_set_timeout($sock, 2);
|
||||
$resp = @fread($sock, 8192);
|
||||
fclose($sock);
|
||||
$count = 0;
|
||||
if (preg_match('/(\d+)/', (string)$resp, $m)) $count = (int)$m[1];
|
||||
$out[] = ['key' => 'falkor', 'label' => 'FalkorDB (dbn_client_graph)',
|
||||
'value' => $count . ' Document nodes',
|
||||
'status' => 'ok',
|
||||
'detail' => 'Colin @ 10.0.2.10:6379 — populated during ingest'];
|
||||
} else {
|
||||
$out[] = ['key' => 'falkor', 'label' => 'FalkorDB', 'value' => 'unreachable', 'status' => 'warn',
|
||||
'detail' => 'Graph features hidden; ingest still works.'];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$out[] = ['key' => 'falkor', 'label' => 'FalkorDB', 'value' => 'error', 'status' => 'warn', 'detail' => $e->getMessage()];
|
||||
}
|
||||
|
||||
/* On-disk storage usage */
|
||||
try {
|
||||
$root = dbnToolsEnv('DBN_TOOLS_UPLOAD_ROOT', '')
|
||||
?: (is_dir('/home/dobetternorge/uploads') ? '/home/dobetternorge/uploads' : DBN_TOOLS_ROOT . '/uploads');
|
||||
$clientDir = rtrim($root, '/') . '/' . $clientId;
|
||||
$total = 0; $files = 0;
|
||||
if (is_dir($clientDir)) {
|
||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($clientDir, FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($it as $f) {
|
||||
if ($f->isFile()) { $total += $f->getSize(); $files++; }
|
||||
}
|
||||
}
|
||||
$human = $total < 1024*1024 ? round($total/1024) . ' KB'
|
||||
: ($total < 1024*1024*1024 ? round($total/1024/1024, 1) . ' MB' : round($total/1024/1024/1024, 2) . ' GB');
|
||||
$out[] = ['key' => 'storage', 'label' => 'Original file storage',
|
||||
'value' => $human . ' · ' . $files . ' files',
|
||||
'status' => is_dir($clientDir) ? 'ok' : 'warn',
|
||||
'detail' => $clientDir];
|
||||
} catch (Throwable $e) {
|
||||
$out[] = ['key' => 'storage', 'label' => 'Storage', 'value' => 'error', 'status' => 'warn', 'detail' => $e->getMessage()];
|
||||
}
|
||||
|
||||
dbnToolsRespond([
|
||||
'ok' => true,
|
||||
'tenant' => [
|
||||
'client_id' => $clientId,
|
||||
'corpus_id' => (int)$tenant['corpus_id'],
|
||||
'user_id' => (int)$tenant['client_user_id'],
|
||||
],
|
||||
'sections' => $out,
|
||||
]);
|
||||
Reference in New Issue
Block a user