feat(ui): implement public status endpoint and update UI components
Some checks failed
CI / build-and-test (push) Failing after -30m49s

* add public status handler and response types
* modify status API to restrict access and update client tracking
* adjust UI components to display public status information
* update routing to include public status endpoint
This commit is contained in:
2026-04-27 00:23:06 +02:00
parent e208c62df3
commit 537e65ea6d
10 changed files with 182 additions and 32 deletions

View File

@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import LoginPage from './components/auth/LoginPage.svelte';
import AdminShell from './components/shell/AdminShell.svelte';
import type { ShellPage, StatusResponse } from './types';
import type { PublicStatusResponse, ShellPage, StatusResponse } from './types';
import { fromStore } from 'svelte/store';
import {
ensureApiURL,
@@ -20,6 +20,9 @@
let data = $state<StatusResponse | null>(null);
let loading = $state(false);
let error = $state('');
let publicStatus = $state<PublicStatusResponse | null>(null);
let publicStatusLoading = $state(false);
let publicStatusError = $state('');
let currentPage = $state<ShellPage>('dashboard');
ensureApiURL(import.meta.env.VITE_API_URL);
@@ -57,7 +60,7 @@
const token = await loginWithCredentials(username, password);
await GlobalStateStore.getState().login(token, { username });
authMessage = 'Login successful.';
await loadStatus();
await loadDashboardStatus();
} catch (err) {
authError = err instanceof Error ? err.message : 'Login failed.';
} finally {
@@ -88,7 +91,7 @@
authMessage = 'OAuth login complete. Welcome back.';
window.history.replaceState({}, '', '/');
await loadStatus();
await loadDashboardStatus();
} catch (err) {
authError = err instanceof Error ? err.message : 'OAuth callback failed.';
} finally {
@@ -100,14 +103,23 @@
await GlobalStateStore.getState().logout();
authMessage = 'Logged out.';
authError = '';
await loadPublicStatus();
}
async function loadStatus(): Promise<void> {
async function loadDashboardStatus(): Promise<void> {
loading = true;
error = '';
try {
const response = await fetch('/api/status');
const token = GlobalStateStore.getState().session?.authToken;
if (!token) {
throw new Error('Missing auth token for dashboard status.');
}
const response = await fetch('/api/status', {
headers: {
Authorization: `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`Status request failed with ${response.status}`);
}
@@ -141,6 +153,28 @@
}
}
async function loadPublicStatus(): Promise<void> {
publicStatusLoading = true;
publicStatusError = '';
try {
const response = await fetch('/status');
if (!response.ok) {
throw new Error(`Public status request failed with ${response.status}`);
}
const raw = (await response.json()) as Partial<PublicStatusResponse> | null;
publicStatus = {
connected_count: raw?.connected_count ?? 0,
connected_window: raw?.connected_window ?? 'last 10 minutes',
entries: Array.isArray(raw?.entries) ? raw.entries : []
};
} catch (err) {
publicStatusError = err instanceof Error ? err.message : 'Failed to load public status';
} finally {
publicStatusLoading = false;
}
}
onMount(async () => {
if (typeof window !== 'undefined') {
setCurrentPath(window.location.pathname);
@@ -153,7 +187,11 @@
await GlobalStateStore.getState().fetchData();
await loadStatus();
if (GlobalStateStore.getState().isLoggedIn()) {
await loadDashboardStatus();
return;
}
await loadPublicStatus();
});
</script>
@@ -169,9 +207,9 @@
{authBusy}
{authError}
{authMessage}
statusData={data}
statusLoading={loading}
statusError={error}
statusData={publicStatus}
statusLoading={publicStatusLoading}
statusError={publicStatusError}
onlogin={handleCredentialLogin}
/>
{:else}
@@ -182,7 +220,7 @@
{error}
onlogout={logout}
onnavigate={(page) => { currentPage = page; }}
onrefresh={loadStatus}
onrefresh={loadDashboardStatus}
/>
{/if}
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { StatusResponse } from "../../types";
import type { PublicStatusResponse } from "../../types";
type IntelligenceCard = {
id: string;
title: string;
@@ -16,7 +16,7 @@
error,
}: {
isOAuthCallback: boolean;
data: StatusResponse | null;
data: PublicStatusResponse | null;
loading: boolean;
error: string;
} = $props();
@@ -143,10 +143,24 @@
<p class="mt-2 text-sm font-medium text-rose-300">Unavailable</p>
<p class="mt-2 text-xs text-rose-200/80">{error}</p>
{:else if data}
<p class="mt-2 text-2xl font-semibold text-white">{data.version}</p>
<p class="mt-2 text-sm text-slate-400">
{data.connected_count} connected in {data.connected_window}
<p class="mt-2 text-2xl font-semibold text-white">
{data.connected_count}
</p>
<p class="mt-2 text-sm text-slate-400">
connected in {data.connected_window}
</p>
{#if data.entries.length > 0}
<p class="mt-2 text-xs text-slate-400">
Clients:
{#each data.entries.slice(0, 3) as client, idx}
<code class="text-slate-200">{client.key_id}</code>{idx <
Math.min(data.entries.length, 3) - 1
? ", "
: ""}
{/each}
{data.entries.length > 3 ? " …" : ""}
</p>
{/if}
<a
href="/status"
class="mt-3 inline-flex items-center rounded-lg border border-emerald-300/30 bg-emerald-400/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.12em] text-emerald-100 transition hover:border-emerald-300/50 hover:bg-emerald-400/20"

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import LoginInfoPanel from './LoginInfoPanel.svelte';
import LoginPanel from './LoginPanel.svelte';
import type { StatusResponse } from '../../types';
import type { PublicStatusResponse } from '../../types';
const {
isOAuthCallback,
@@ -19,7 +19,7 @@
authBusy: boolean;
authError: string;
authMessage: string;
statusData: StatusResponse | null;
statusData: PublicStatusResponse | null;
statusLoading: boolean;
statusError: string;
onlogin: (username: string, password: string) => void;

View File

@@ -37,6 +37,18 @@ export type StatusResponse = {
metrics: AccessMetrics;
};
export type PublicStatusClient = {
key_id: string;
request_count: number;
last_accessed_at: string;
};
export type PublicStatusResponse = {
connected_count: number;
connected_window: string;
entries: PublicStatusClient[];
};
export type NavItem = {
id: string;
label: string;

View File

@@ -11,6 +11,7 @@ export default defineConfig({
port: 5173,
proxy: {
'/api': backendTarget,
'/status': backendTarget,
'/healthz': backendTarget,
'/readyz': backendTarget,
'/llm': backendTarget,