Files
amcs/ui/src/App.svelte
T
warkanum 9e6d05e055
CI / build-and-test (push) Failing after -31m24s
feat(ui): add content editor components for skills and thoughts
* Implement ContentEditorField for inline editing of content
* Create ContentEditorModal for editing content in a modal
* Introduce FormerShell for managing forms related to skills and thoughts
* Enhance SkillsPage and ThoughtsPage with new components for better content management
2026-05-02 19:35:27 +02:00

227 lines
7.0 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import LoginPage from './components/auth/LoginPage.svelte';
import AdminShell from './components/shell/AdminShell.svelte';
import type { PublicStatusResponse, ShellPage, StatusResponse } from './types';
import { fromStore } from 'svelte/store';
import {
ensureApiURL,
exchangeOAuthCode,
GlobalStateStore,
isLoggedInStore,
loginWithCredentials,
setCurrentPath
} from './shellState';
let authMessage = $state('');
let authError = $state('');
let authBusy = $state(false);
let callbackBusy = $state(false);
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);
GlobalStateStore.setState({
onFetchSession: async (state) => {
const token = state.session?.authToken;
if (!token) return {};
const res = await fetch('/api/rs/public/projects', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
operation: 'read',
options: { limit: 1 }
})
});
if (!res.ok) return { session: { loggedIn: false } };
return { session: { loggedIn: true, authToken: token } };
}
});
const isLoggedIn = fromStore(isLoggedInStore);
const currentPath = $derived(typeof window !== 'undefined' ? window.location.pathname : '/');
const isOAuthCallback = $derived(currentPath === '/oauth/callback');
async function handleCredentialLogin(username: string, password: string): Promise<void> {
authBusy = true;
authError = '';
authMessage = '';
try {
const token = await loginWithCredentials(username, password);
await GlobalStateStore.getState().login(token, { username });
authMessage = 'Login successful.';
await loadDashboardStatus();
} catch (err) {
authError = err instanceof Error ? err.message : 'Login failed.';
} finally {
authBusy = false;
}
}
async function finishOAuthLogin(): Promise<void> {
callbackBusy = true;
authError = '';
authMessage = '';
try {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const returnedState = params.get('state');
const oauthError = params.get('error');
if (oauthError) {
throw new Error(`OAuth login failed: ${oauthError}`);
}
if (!code || !returnedState) {
throw new Error('OAuth callback is missing code or state.');
}
const token = await exchangeOAuthCode(code, returnedState);
await GlobalStateStore.getState().login(token, { username: 'OAuth operator' });
authMessage = 'OAuth login complete. Welcome back.';
window.history.replaceState({}, '', '/');
await loadDashboardStatus();
} catch (err) {
authError = err instanceof Error ? err.message : 'OAuth callback failed.';
} finally {
callbackBusy = false;
}
}
async function logout(): Promise<void> {
await GlobalStateStore.getState().logout();
authMessage = 'Logged out.';
authError = '';
await loadPublicStatus();
}
async function loadDashboardStatus(): Promise<void> {
loading = true;
error = '';
try {
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}`);
}
const raw = (await response.json()) as Partial<StatusResponse> | null;
data = {
title: raw?.title ?? 'AMCS',
description: raw?.description ?? '',
version: raw?.version ?? 'unknown',
build_date: raw?.build_date ?? 'unknown',
commit: raw?.commit ?? 'unknown',
connected_count: raw?.connected_count ?? 0,
total_known: raw?.total_known ?? 0,
connected_window: raw?.connected_window ?? 'last 10 minutes',
oauth_enabled: !!raw?.oauth_enabled,
entries: Array.isArray(raw?.entries) ? raw.entries : [],
metrics: {
total_requests: raw?.metrics?.total_requests ?? 0,
unique_principals: raw?.metrics?.unique_principals ?? 0,
unique_ips: raw?.metrics?.unique_ips ?? 0,
unique_agents: raw?.metrics?.unique_agents ?? 0,
unique_tools: raw?.metrics?.unique_tools ?? 0,
top_ips: Array.isArray(raw?.metrics?.top_ips) ? raw.metrics.top_ips : [],
top_agents: Array.isArray(raw?.metrics?.top_agents) ? raw.metrics.top_agents : [],
top_tools: Array.isArray(raw?.metrics?.top_tools) ? raw.metrics.top_tools : []
}
};
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load status';
} finally {
loading = false;
}
}
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);
}
if (isOAuthCallback) {
await finishOAuthLogin();
return;
}
await GlobalStateStore.getState().fetchData();
if (GlobalStateStore.getState().isLoggedIn()) {
await loadDashboardStatus();
return;
}
await loadPublicStatus();
});
</script>
<svelte:head>
<title>AMCS Admin</title>
</svelte:head>
<div data-theme="amcs" class="min-h-screen bg-slate-950 text-slate-100">
{#if !isLoggedIn.current}
<LoginPage
{isOAuthCallback}
{callbackBusy}
{authBusy}
{authError}
{authMessage}
statusData={publicStatus}
statusLoading={publicStatusLoading}
statusError={publicStatusError}
onlogin={handleCredentialLogin}
/>
{:else}
<AdminShell
{currentPage}
{data}
{loading}
{error}
onlogout={logout}
onnavigate={(page) => { currentPage = page; }}
onrefresh={loadDashboardStatus}
/>
{/if}
</div>