feat(dashboard): add corpus dashboard at /dashboard/

Full private corpus dashboard for tools.dobetternorge.no users — each SSO
account gets an auto-provisioned CaveauAI tenant (clients row, corpus) on
first visit. Includes upload (file/paste/URL), RAG chat with SSE streaming
and citation chips, document CRUD, FalkorDB graph relations tab, and
improved save-from-tool flow with tag/preview support.

- dashboard/{index,documents,document,upload,chat,settings}.php
- api/dashboard/{corpus-init,documents,upload,ingest-status,chat-stream,
  save-from-tool,graph}.php
- includes/{CorpusProvision,layout_dashboard,layout_dashboard_footer}.php
- assets/css/dashboard.css  assets/js/corpus-save.js (routing upgrade)
- includes/{bootstrap,layout}.php extended for dashboard provisioning

Migration 141 (clients.dbn_sso_uid + import_method enum) applied on chloe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 17:15:40 +02:00
parent 83fc71414f
commit 06d01a3bce
20 changed files with 2632 additions and 28 deletions
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* GET /api/dashboard/graph.php?action=cites|cited_by|implements|chain&doc_id=N&limit=20&depth=2
*
* Wraps ai-portal/lib/ai/LegalGraphAgent for the dashboard. Reads the FalkorDB
* `bnl_legal` graph on Colin (10.0.2.10:6379). Public graph metadata — no
* sensitive content — but we still gate on dashboard auth to avoid being a
* generic open proxy.
*
* Response shape mirrors ai-portal/api/graph-search.php:
* { ok, action, doc_id, count, results: [ {rel_type, doc_id, title, ...}, ...] }
*/
declare(strict_types=1);
require_once dirname(__DIR__, 2) . '/includes/bootstrap.php';
dbnToolsRequireMethod('GET');
dbnToolsRequireAuth();
// Don't require dashboard provisioning here — graph is public metadata.
$action = trim((string)($_GET['action'] ?? ''));
$docId = (int)($_GET['doc_id'] ?? 0);
$limit = max(1, min(100, (int)($_GET['limit'] ?? 20)));
$depth = max(1, min(3, (int)($_GET['depth'] ?? 2)));
$validActions = ['cites', 'cited_by', 'implements', 'chain'];
if (!in_array($action, $validActions, true)) {
dbnToolsError(
'action must be one of: ' . implode(', ', $validActions),
400, 'invalid_action', ['actions' => $validActions]
);
}
if ($docId <= 0) {
dbnToolsError('doc_id must be a positive integer.', 400, 'missing_doc_id');
}
$root = dbnToolsAiPortalRoot();
$graphFile = $root . '/lib/ai/GraphClient.php';
$agentFile = $root . '/lib/ai/LegalGraphAgent.php';
if (!is_file($graphFile) || !is_file($agentFile)) {
dbnToolsError('Graph backend not installed.', 503, 'graph_unavailable');
}
require_once $graphFile;
require_once $agentFile;
try {
$config = file_exists('/etc/bnl/config.php') ? include '/etc/bnl/config.php' : [];
$host = (string)($config['falkordb']['host'] ?? dbnToolsEnv('DBN_FALKORDB_HOST', '10.0.2.10'));
$port = (int) ($config['falkordb']['port'] ?? (int)dbnToolsEnv('DBN_FALKORDB_PORT', '6379'));
$pass = (string)($config['falkordb']['password'] ?? dbnToolsEnv('DBN_FALKORDB_PASSWORD', ''));
$client = new GraphClient($host, $port, $pass);
$agent = new LegalGraphAgent($client);
$results = match ($action) {
'cites' => $agent->cites($docId, $limit),
'cited_by' => $agent->citedBy($docId, $limit),
'implements' => $agent->implements($docId, $limit),
'chain' => $agent->chain($docId, $depth),
};
} catch (Throwable $e) {
dbnToolsRespond([
'ok' => true,
'action' => $action,
'doc_id' => $docId,
'count' => 0,
'results' => [],
'warning' => 'Graph backend unavailable: ' . $e->getMessage(),
]);
}
dbnToolsRespond([
'ok' => true,
'action' => $action,
'doc_id' => $docId,
'count' => count($results),
'results' => $results,
]);