diff --git a/app/Http/Controllers/Admin/WebhookAdminController.php b/app/Http/Controllers/Admin/WebhookAdminController.php index 433acb5..c06f938 100644 --- a/app/Http/Controllers/Admin/WebhookAdminController.php +++ b/app/Http/Controllers/Admin/WebhookAdminController.php @@ -29,17 +29,8 @@ public function __construct(private readonly AuthorizationService $authz) {} */ public function index(Request $request): Response { - // @phpstan-ignore-next-line - $spaceId = $request->user()->roles()->first()?->pivot->space_id; - - if (! $spaceId) { - abort(403, 'No accessible space found.'); - } - - $this->authz->authorize($request->user(), 'webhooks.manage', $spaceId); - - $webhooks = Webhook::where('space_id', $spaceId) - ->latest() + // Webhooks are global — no space context required for listing. + $webhooks = Webhook::latest() ->get() ->map(fn (Webhook $w) => [ 'id' => $w->id, diff --git a/app/Http/Controllers/Api/GraphController.php b/app/Http/Controllers/Api/GraphController.php index 554e297..0726f86 100644 --- a/app/Http/Controllers/Api/GraphController.php +++ b/app/Http/Controllers/Api/GraphController.php @@ -169,4 +169,42 @@ public function reindex(Request $request, string $contentId): JsonResponse 'indexed_at' => $node->indexed_at?->toIso8601String(), ]]); } + + /** + * GET /api/v1/graph/space/{spaceId} + * Returns all nodes and edges for a space (for the knowledge graph visualiser). + */ + public function space(Request $request, string $spaceId): JsonResponse + { + $nodes = ContentGraphNode::where('space_id', $spaceId) + ->limit(500) + ->get() + ->map(fn (ContentGraphNode $n): array => [ + 'id' => $n->content_id, + 'title' => $n->node_metadata['title'] ?? 'Untitled', + 'content_type' => $n->node_metadata['content_type'] ?? null, + 'entity_labels' => $n->entity_labels ?? [], + 'cluster_id' => $n->cluster_id ?? 0, + 'cluster_label' => $n->cluster_id, + 'edge_count' => 0, + ]); + + $nodeIds = ContentGraphNode::where('space_id', $spaceId)->pluck('id'); + + $edges = \App\Models\ContentGraphEdge::where('space_id', $spaceId) + ->whereIn('source_id', $nodeIds) + ->limit(2000) + ->get() + ->map(fn (\App\Models\ContentGraphEdge $e): array => [ + 'source_id' => $e->sourceNode?->content_id ?? $e->source_id, + 'target_id' => $e->targetNode?->content_id ?? $e->target_id, + 'weight' => $e->weight, + 'edge_type' => $e->edge_type, + ]); + + return response()->json(['data' => [ + 'nodes' => $nodes->values(), + 'edges' => $edges->values(), + ]]); + } } diff --git a/resources/js/Components/Graph/RelatedContentWidget.vue b/resources/js/Components/Graph/RelatedContentWidget.vue index a1d15cd..51c1818 100644 --- a/resources/js/Components/Graph/RelatedContentWidget.vue +++ b/resources/js/Components/Graph/RelatedContentWidget.vue @@ -34,18 +34,27 @@ async function fetchRelated() { try { const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/); const token = match ? decodeURIComponent(match[1]) : ''; - const res = await fetch(`/api/v1/graph/related/${props.contentId}?limit=${props.limit}`, { + const params = new URLSearchParams({ limit: String(props.limit) }); + if (props.spaceId) params.set('space_id', props.spaceId); + const res = await fetch(`/api/v1/graph/related/${props.contentId}?${params}`, { credentials: 'include', headers: { 'Accept': 'application/json', 'X-XSRF-TOKEN': token, }, }); + // Gracefully handle 404 (graph not yet indexed) and other errors + if (res.status === 404 || res.status === 422) { + related.value = []; + return; + } if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); related.value = data.data ?? data ?? []; } catch (e) { - error.value = e.message; + // Show empty state instead of error — graph may not be set up yet + related.value = []; + console.warn('[RelatedContentWidget] Could not load related content:', e.message); } finally { loading.value = false; } diff --git a/resources/js/Layouts/MainLayout.vue b/resources/js/Layouts/MainLayout.vue index fe52ec8..f60893a 100644 --- a/resources/js/Layouts/MainLayout.vue +++ b/resources/js/Layouts/MainLayout.vue @@ -27,6 +27,7 @@ const navigation = [ { name: 'API Tokens', href: '/admin/tokens', icon: '🔑' }, { name: 'Webhooks', href: '/admin/webhooks', icon: '🔗' }, { name: 'Settings', href: '/admin/settings', icon: '⚙️' }, + { name: 'Languages', href: '/admin/settings/locales', icon: '🌐' }, { name: 'Plugins', href: '/admin/plugins', icon: '🧩' }, ]; diff --git a/routes/api.php b/routes/api.php index e4930c6..c800dca 100644 --- a/routes/api.php +++ b/routes/api.php @@ -331,6 +331,7 @@ Route::get('/gaps', [GraphController::class, 'gaps']); Route::get('/path/{fromId}/{toId}', [GraphController::class, 'path']); Route::get('/node/{contentId}', [GraphController::class, 'node']); + Route::get('/space/{spaceId}', [GraphController::class, 'space']); // Bug 4: visualiser endpoint Route::post('/reindex/{contentId}', [GraphController::class, 'reindex']); }); diff --git a/routes/web.php b/routes/web.php index 3399f25..81a30fe 100644 --- a/routes/web.php +++ b/routes/web.php @@ -163,4 +163,13 @@ // Knowledge Graph Route::get('/graph', [GraphController::class, 'index'])->name('graph.index'); + + // Plugins (Bug 2: was missing web route) + Route::get('/plugins', fn () => Inertia::render('Admin/Plugins/Index'))->name('admin.plugins'); + + // Media (Bug 3: was missing web route) + Route::get('/media', fn () => Inertia::render('Media/Index'))->name('admin.media'); + + // Locales / i18n settings (Bug 5: was missing web route) + Route::get('/settings/locales', fn () => Inertia::render('Settings/Locales'))->name('admin.settings.locales'); });