Files
dobetternorge-tools/api/dashboard/diagnostics.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

166 lines
7.1 KiB
PHP

<?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,
]);