9e6d05e055
CI / build-and-test (push) Failing after -31m24s
* 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
227 lines
7.0 KiB
Svelte
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>
|