feat(ui): add origin-style admin shell scaffold
This commit is contained in:
@@ -19,5 +19,14 @@
|
|||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.2"
|
"vite": "^6.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry/svelte": "^10.49.0",
|
||||||
|
"@skeletonlabs/skeleton": "^4.15.2",
|
||||||
|
"@skeletonlabs/skeleton-svelte": "^4.15.2",
|
||||||
|
"@tanstack/svelte-virtual": "^3.13.24",
|
||||||
|
"@warkypublic/artemis-kit": "file:../../artemis-kit",
|
||||||
|
"@warkypublic/resolvespec-js": "^1.0.1",
|
||||||
|
"@warkypublic/svelix": "^0.1.31"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3150
ui/pnpm-lock.yaml
generated
3150
ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from 'svelte';
|
||||||
|
import {
|
||||||
|
Former,
|
||||||
|
FormerRestHeadSpecAPI,
|
||||||
|
InlineWrapper,
|
||||||
|
PasswordInputCtrl,
|
||||||
|
TextInputCtrl,
|
||||||
|
getApiURL
|
||||||
|
} from '@warkypublic/svelix';
|
||||||
|
import { ensureApiURL, GlobalStateStore } from './shellState';
|
||||||
|
|
||||||
|
type LoginResult = {
|
||||||
|
token?: string;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type AccessEntry = {
|
type AccessEntry = {
|
||||||
key_id: string;
|
key_id: string;
|
||||||
@@ -22,241 +36,354 @@
|
|||||||
entries: AccessEntry[];
|
entries: AccessEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
let data: StatusResponse | null = null;
|
type NavItem = {
|
||||||
let loading = true;
|
id: string;
|
||||||
let error = "";
|
label: string;
|
||||||
|
description: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const quickLinks = [
|
const navItems: NavItem[] = [
|
||||||
{ href: "/llm", label: "LLM Instructions" },
|
{
|
||||||
{ href: "/healthz", label: "Health Check" },
|
id: 'dashboard',
|
||||||
{ href: "/readyz", label: "Readiness Check" },
|
label: 'Dashboard',
|
||||||
|
description: 'System overview and status snapshots.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'projects',
|
||||||
|
label: 'Projects',
|
||||||
|
description: 'First management module for AMCS projects.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'thoughts',
|
||||||
|
label: 'Thoughts',
|
||||||
|
description: 'Thought management arrives after projects.',
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'files',
|
||||||
|
label: 'Files',
|
||||||
|
description: 'File inventory and attachment views.',
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
async function loadStatus() {
|
let values = $state<{ username: string; password: string }>({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
let authMessage = $state('');
|
||||||
|
let authError = $state('');
|
||||||
|
let data = $state<StatusResponse | null>(null);
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state('');
|
||||||
|
let currentPage = $state<'dashboard' | 'projects'>('dashboard');
|
||||||
|
|
||||||
|
ensureApiURL(import.meta.env.VITE_API_URL);
|
||||||
|
|
||||||
|
const onAPICall = $derived(
|
||||||
|
FormerRestHeadSpecAPI({
|
||||||
|
url: `${getApiURL()}/login`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoggedIn = $derived(GlobalStateStore.isLoggedIn());
|
||||||
|
|
||||||
|
async function handleLogin(value: LoginResult): Promise<void> {
|
||||||
|
const token = value?.token?.trim();
|
||||||
|
if (!token) {
|
||||||
|
authError = 'Login succeeded but no token was returned by the API.';
|
||||||
|
authMessage = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await GlobalStateStore.getState().login(token, {
|
||||||
|
username: value?.username ?? values.username
|
||||||
|
});
|
||||||
|
|
||||||
|
authMessage = `Welcome back ${value?.username ?? values.username ?? 'operator'}.`;
|
||||||
|
authError = '';
|
||||||
|
await loadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout(): Promise<void> {
|
||||||
|
await GlobalStateStore.getState().logout();
|
||||||
|
authMessage = 'Logged out.';
|
||||||
|
authError = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStatus(): Promise<void> {
|
||||||
loading = true;
|
loading = true;
|
||||||
error = "";
|
error = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/status");
|
const response = await fetch('/api/status');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Status request failed with ${response.status}`);
|
throw new Error(`Status request failed with ${response.status}`);
|
||||||
}
|
}
|
||||||
data = (await response.json()) as StatusResponse;
|
data = (await response.json()) as StatusResponse;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : "Failed to load status";
|
error = err instanceof Error ? err.message : 'Failed to load status';
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value: string) {
|
function formatDate(value: string): string {
|
||||||
return new Date(value).toLocaleString();
|
return new Date(value).toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(loadStatus);
|
onMount(async () => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
await loadStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>AMCS</title>
|
<title>AMCS Admin</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen bg-slate-950 text-slate-100">
|
<div class="min-h-screen bg-slate-950 text-slate-100">
|
||||||
<main
|
{#if !isLoggedIn}
|
||||||
class="mx-auto flex min-h-screen max-w-7xl flex-col px-4 py-6 sm:px-6 lg:px-8"
|
<main class="mx-auto flex min-h-screen max-w-6xl items-center px-4 py-10 sm:px-6 lg:px-8">
|
||||||
>
|
<section class="grid w-full gap-8 lg:grid-cols-[1.15fr_0.85fr]">
|
||||||
<section
|
<div class="rounded-3xl border border-cyan-400/20 bg-slate-900/80 p-8 shadow-2xl shadow-slate-950/40">
|
||||||
class="overflow-hidden rounded-3xl border border-white/10 bg-slate-900 shadow-2xl shadow-slate-950/40"
|
<div class="inline-flex items-center gap-2 rounded-full border border-cyan-400/20 bg-cyan-400/10 px-3 py-1 text-sm font-medium text-cyan-200">
|
||||||
>
|
<span class="h-2 w-2 rounded-full bg-emerald-400"></span>
|
||||||
<img
|
AMCS Control Interface
|
||||||
src="/images/project.jpg"
|
|
||||||
alt="Avelon Memory Crystal"
|
|
||||||
class="h-64 w-full object-cover object-center sm:h-80"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="grid gap-8 p-6 sm:p-8 lg:grid-cols-[1.6fr_1fr] lg:p-10">
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div
|
|
||||||
class="inline-flex items-center gap-2 rounded-full border border-cyan-400/20 bg-cyan-400/10 px-3 py-1 text-sm font-medium text-cyan-200"
|
|
||||||
>
|
|
||||||
<span class="h-2 w-2 rounded-full bg-emerald-400"></span>
|
|
||||||
Avalon Memory Crystal Server
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1
|
|
||||||
class="text-3xl font-semibold tracking-tight text-white sm:text-4xl"
|
|
||||||
>
|
|
||||||
Avelon Memory Crystal Server (AMCS)
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
class="mt-3 max-w-3xl text-base leading-7 text-slate-300 sm:text-lg"
|
|
||||||
>
|
|
||||||
{data?.description ??
|
|
||||||
"AMCS is a memory server that captures, links, and retrieves structured project thoughts for AI assistants using semantic search, summaries, and MCP tools."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h1 class="mt-6 text-4xl font-semibold tracking-tight text-white">Login</h1>
|
||||||
<div class="flex flex-wrap gap-3">
|
<p class="mt-3 max-w-2xl text-base leading-7 text-slate-300">
|
||||||
{#each quickLinks as link}
|
Origin-style operator access for the AMCS admin interface. ResolveSpec OAuth stays the auth brain;
|
||||||
<a
|
this shell just gives us the front door.
|
||||||
class="inline-flex items-center justify-center rounded-xl border border-cyan-300/20 bg-cyan-400/10 px-4 py-2 text-sm font-semibold text-cyan-100 transition hover:border-cyan-300/40 hover:bg-cyan-400/20"
|
|
||||||
href={link.href}>{link.label}</a
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
{#if data?.oauth_enabled}
|
|
||||||
<a
|
|
||||||
class="inline-flex items-center justify-center rounded-xl border border-violet-300/20 bg-violet-400/10 px-4 py-2 text-sm font-semibold text-violet-100 transition hover:border-violet-300/40 hover:bg-violet-400/20"
|
|
||||||
href="/oauth-authorization-server">OAuth Authorization Server</a
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
|
||||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
|
||||||
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">
|
|
||||||
Connected users
|
|
||||||
</p>
|
|
||||||
<p class="mt-2 text-3xl font-semibold text-white">
|
|
||||||
{data?.connected_count ?? "—"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
|
||||||
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">
|
|
||||||
Known principals
|
|
||||||
</p>
|
|
||||||
<p class="mt-2 text-3xl font-semibold text-white">
|
|
||||||
{data?.total_known ?? "—"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
|
||||||
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">
|
|
||||||
Version
|
|
||||||
</p>
|
|
||||||
<p class="mt-2 break-all text-2xl font-semibold text-white">
|
|
||||||
{data?.version ?? "—"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside
|
|
||||||
class="space-y-4 rounded-2xl border border-white/10 bg-slate-950/50 p-5"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-semibold text-white">Build details</h2>
|
|
||||||
<p class="mt-1 text-sm text-slate-400">The same status info.</p>
|
|
||||||
</div>
|
|
||||||
<dl class="space-y-3 text-sm text-slate-300">
|
|
||||||
<div>
|
|
||||||
<dt class="text-slate-500">Build date</dt>
|
|
||||||
<dd class="mt-1 font-medium text-white">
|
|
||||||
{data?.build_date ?? "unknown"}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt class="text-slate-500">Commit</dt>
|
|
||||||
<dd
|
|
||||||
class="mt-1 break-all rounded-lg bg-white/5 px-3 py-2 font-mono text-xs text-cyan-100"
|
|
||||||
>
|
|
||||||
{data?.commit ?? "unknown"}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt class="text-slate-500">Connected window</dt>
|
|
||||||
<dd class="mt-1 font-medium text-white">
|
|
||||||
{data?.connected_window ?? "last 10 minutes"}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
class="mt-6 rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20 sm:p-8"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-2xl font-semibold text-white">Recent access</h2>
|
|
||||||
<p class="mt-1 text-sm text-slate-400">
|
|
||||||
Authenticated principals AMCS has seen recently.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-slate-200 transition hover:bg-white/10"
|
|
||||||
on:click={loadStatus}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if loading}
|
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||||
<div
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
class="mt-6 rounded-2xl border border-dashed border-white/10 bg-slate-950/40 px-4 py-10 text-center text-slate-400"
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Primary module</p>
|
||||||
>
|
<p class="mt-2 text-2xl font-semibold text-white">Projects</p>
|
||||||
Loading status…
|
<p class="mt-2 text-sm text-slate-400">Projects are the first real admin screen in this rollout.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
<div
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">UI direction</p>
|
||||||
class="mt-6 rounded-2xl border border-rose-400/30 bg-rose-400/10 px-4 py-6 text-sm text-rose-100"
|
<p class="mt-2 text-2xl font-semibold text-white">Origin-like</p>
|
||||||
>
|
<p class="mt-2 text-sm text-slate-400">Login and page structure mapped toward Origin patterns.</p>
|
||||||
<p class="font-semibold">Couldn’t load the status snapshot.</p>
|
</div>
|
||||||
<p class="mt-1 text-rose-100/80">{error}</p>
|
|
||||||
</div>
|
|
||||||
{:else if data && data.entries.length === 0}
|
|
||||||
<div
|
|
||||||
class="mt-6 rounded-2xl border border-dashed border-white/10 bg-slate-950/40 px-4 py-10 text-center text-slate-400"
|
|
||||||
>
|
|
||||||
No authenticated access recorded yet.
|
|
||||||
</div>
|
|
||||||
{:else if data}
|
|
||||||
<div class="mt-6 overflow-hidden rounded-2xl border border-white/10">
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table
|
|
||||||
class="min-w-full divide-y divide-white/10 text-left text-sm text-slate-300"
|
|
||||||
>
|
|
||||||
<thead
|
|
||||||
class="bg-white/5 text-xs uppercase tracking-[0.2em] text-slate-500"
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<th class="px-4 py-3 font-medium">Principal</th>
|
|
||||||
<th class="px-4 py-3 font-medium">Last accessed</th>
|
|
||||||
<th class="px-4 py-3 font-medium">Last path</th>
|
|
||||||
<th class="px-4 py-3 font-medium">Agent</th>
|
|
||||||
<th class="px-4 py-3 font-medium">Requests</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-white/5 bg-slate-950/30">
|
|
||||||
{#each data.entries as entry}
|
|
||||||
<tr class="hover:bg-white/[0.03]">
|
|
||||||
<td class="px-4 py-3 align-top"
|
|
||||||
><code
|
|
||||||
class="rounded bg-white/5 px-2 py-1 font-mono text-xs text-cyan-100"
|
|
||||||
>{entry.key_id}</code
|
|
||||||
></td
|
|
||||||
>
|
|
||||||
<td class="px-4 py-3 align-top text-slate-200"
|
|
||||||
>{formatDate(entry.last_accessed_at)}</td
|
|
||||||
>
|
|
||||||
<td class="px-4 py-3 align-top"
|
|
||||||
><code class="text-slate-100">{entry.last_path}</code></td
|
|
||||||
>
|
|
||||||
<td class="px-4 py-3 align-top text-slate-400 text-xs max-w-[16rem] truncate"
|
|
||||||
>{entry.user_agent ?? "—"}</td
|
|
||||||
>
|
|
||||||
<td class="px-4 py-3 align-top font-semibold text-white"
|
|
||||||
>{entry.request_count}</td
|
|
||||||
>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</section>
|
<div class="rounded-3xl border border-white/10 bg-slate-900 p-6 shadow-xl shadow-slate-950/30 sm:p-8">
|
||||||
</main>
|
<h2 class="text-xl font-semibold text-white">Operator login</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-400">Authenticate to access AMCS admin pages.</p>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<Former
|
||||||
|
bind:values
|
||||||
|
layout={{ buttonArea: 'none' }}
|
||||||
|
{onAPICall}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleLogin(value as LoginResult);
|
||||||
|
}}
|
||||||
|
onError={(err) => {
|
||||||
|
authError = String(err || 'Failed to login. Please check your credentials and try again.');
|
||||||
|
authMessage = '';
|
||||||
|
}}
|
||||||
|
request="insert"
|
||||||
|
>
|
||||||
|
{#snippet children(state)}
|
||||||
|
<div class="space-y-4">
|
||||||
|
<InlineWrapper label="Email address" required value={state.values?.username ?? ''}>
|
||||||
|
<TextInputCtrl
|
||||||
|
name="username"
|
||||||
|
placeholder="operator@example.com"
|
||||||
|
value={state.values?.username ?? ''}
|
||||||
|
onchange={(v) => state.setState('values', { ...state.values, username: v })}
|
||||||
|
/>
|
||||||
|
</InlineWrapper>
|
||||||
|
|
||||||
|
<InlineWrapper label="Password" required value={state.values?.password ?? ''}>
|
||||||
|
<PasswordInputCtrl
|
||||||
|
name="password"
|
||||||
|
placeholder="Your password"
|
||||||
|
value={state.values?.password ?? ''}
|
||||||
|
onchange={(v) => state.setState('values', { ...state.values, password: v })}
|
||||||
|
/>
|
||||||
|
</InlineWrapper>
|
||||||
|
|
||||||
|
<button type="submit" class="inline-flex w-full items-center justify-center rounded-xl border border-cyan-300/20 bg-cyan-400/10 px-4 py-3 text-sm font-semibold text-cyan-100 transition hover:border-cyan-300/40 hover:bg-cyan-400/20">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if authError}
|
||||||
|
<p class="text-sm text-rose-300">{authError}</p>
|
||||||
|
{/if}
|
||||||
|
{#if authMessage}
|
||||||
|
<p class="text-sm text-emerald-300">{authMessage}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Former>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{:else}
|
||||||
|
<div class="grid min-h-screen lg:grid-cols-[17rem_1fr]">
|
||||||
|
<aside class="border-r border-white/10 bg-slate-900/90 p-6">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300">AMCS</p>
|
||||||
|
<h1 class="mt-2 text-2xl font-semibold text-white">Admin</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-400">Origin-style shell, starting with Projects.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="mt-8 space-y-2">
|
||||||
|
{#each navItems as item}
|
||||||
|
<button
|
||||||
|
class={`w-full rounded-2xl border px-4 py-3 text-left transition ${item.disabled ? 'cursor-not-allowed border-white/5 bg-white/[0.02] text-slate-600' : currentPage === item.id ? 'border-cyan-300/30 bg-cyan-400/10 text-cyan-100' : 'border-white/10 bg-white/5 text-slate-200 hover:bg-white/10'}`}
|
||||||
|
disabled={item.disabled}
|
||||||
|
onclick={() => {
|
||||||
|
if (!item.disabled && (item.id === 'dashboard' || item.id === 'projects')) {
|
||||||
|
currentPage = item.id;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="text-sm font-semibold">{item.label}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-400">{item.description}</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="mt-8 inline-flex w-full items-center justify-center rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm font-medium text-slate-200 transition hover:bg-white/10"
|
||||||
|
onclick={logout}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="px-4 py-6 sm:px-6 lg:px-8">
|
||||||
|
{#if currentPage === 'dashboard'}
|
||||||
|
<section class="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20 sm:p-8">
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-white">System overview</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-400">Current AMCS status behind the admin shell.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-slate-200 transition hover:bg-white/10"
|
||||||
|
onclick={loadStatus}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="mt-6 rounded-2xl border border-dashed border-white/10 bg-slate-950/40 px-4 py-10 text-center text-slate-400">
|
||||||
|
Loading status…
|
||||||
|
</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="mt-6 rounded-2xl border border-rose-400/30 bg-rose-400/10 px-4 py-6 text-sm text-rose-100">
|
||||||
|
<p class="font-semibold">Couldn’t load the status snapshot.</p>
|
||||||
|
<p class="mt-1 text-rose-100/80">{error}</p>
|
||||||
|
</div>
|
||||||
|
{:else if data}
|
||||||
|
<div class="mt-6 grid gap-4 sm:grid-cols-3">
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Connected users</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-white">{data.connected_count}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Known principals</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-white">{data.total_known}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Version</p>
|
||||||
|
<p class="mt-2 break-all text-2xl font-semibold text-white">{data.version}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
{:else}
|
||||||
|
<section class="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20 sm:p-8">
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-white">Projects</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-400">First module scaffold. Grid/Form wiring comes next.</p>
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1 text-xs font-medium text-amber-200">
|
||||||
|
Structure phase
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid gap-4 lg:grid-cols-[1.35fr_0.65fr]">
|
||||||
|
<div class="rounded-2xl border border-dashed border-cyan-400/20 bg-cyan-400/5 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-white">Project grid placeholder</h3>
|
||||||
|
<p class="mt-2 text-sm leading-6 text-slate-300">
|
||||||
|
This is the landing zone for the Origin-style projects grid using Svelix and GridlerFull.
|
||||||
|
Next pass: wire ResolveSpec-backed project list, row actions, and editor flow.
|
||||||
|
</p>
|
||||||
|
<ul class="mt-4 space-y-2 text-sm text-slate-400">
|
||||||
|
<li>• Project list and search</li>
|
||||||
|
<li>• Project detail/edit drawer or modal</li>
|
||||||
|
<li>• Create/archive actions</li>
|
||||||
|
<li>• Link-outs to related thoughts and skills</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-white">Build notes</h3>
|
||||||
|
<dl class="mt-4 space-y-3 text-sm text-slate-300">
|
||||||
|
<div>
|
||||||
|
<dt class="text-slate-500">Auth path</dt>
|
||||||
|
<dd class="mt-1">ResolveSpec OAuth packages</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-slate-500">Page pattern</dt>
|
||||||
|
<dd class="mt-1">Mapped toward Origin login and shell</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-slate-500">First module</dt>
|
||||||
|
<dd class="mt-1">Projects</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if data && currentPage === 'dashboard' && data.entries.length > 0}
|
||||||
|
<section class="mt-6 rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20 sm:p-8">
|
||||||
|
<h3 class="text-xl font-semibold text-white">Recent access</h3>
|
||||||
|
<div class="mt-6 overflow-hidden rounded-2xl border border-white/10">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-white/10 text-left text-sm text-slate-300">
|
||||||
|
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 font-medium">Principal</th>
|
||||||
|
<th class="px-4 py-3 font-medium">Last accessed</th>
|
||||||
|
<th class="px-4 py-3 font-medium">Last path</th>
|
||||||
|
<th class="px-4 py-3 font-medium">Agent</th>
|
||||||
|
<th class="px-4 py-3 font-medium">Requests</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-white/5 bg-slate-950/30">
|
||||||
|
{#each data.entries as entry}
|
||||||
|
<tr class="hover:bg-white/[0.03]">
|
||||||
|
<td class="px-4 py-3 align-top"><code class="rounded bg-white/5 px-2 py-1 font-mono text-xs text-cyan-100">{entry.key_id}</code></td>
|
||||||
|
<td class="px-4 py-3 align-top text-slate-200">{formatDate(entry.last_accessed_at)}</td>
|
||||||
|
<td class="px-4 py-3 align-top"><code class="text-slate-100">{entry.last_path}</code></td>
|
||||||
|
<td class="max-w-[16rem] truncate px-4 py-3 align-top text-xs text-slate-400">{entry.user_agent ?? '—'}</td>
|
||||||
|
<td class="px-4 py-3 align-top font-semibold text-white">{entry.request_count}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
46
ui/src/shellState.ts
Normal file
46
ui/src/shellState.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { GlobalStateStore } from '@warkypublic/svelix';
|
||||||
|
|
||||||
|
const normalizeApiURL = (url: string): string => url.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
const resolveApiURL = (envURL?: string): string => {
|
||||||
|
const viteEnvURL =
|
||||||
|
envURL?.trim() ||
|
||||||
|
import.meta.env.VITE_API_URL?.trim() ||
|
||||||
|
import.meta.env.VITE_API_BASE_URL?.trim() ||
|
||||||
|
import.meta.env.VITE_URL?.trim();
|
||||||
|
|
||||||
|
if (viteEnvURL) return normalizeApiURL(viteEnvURL);
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return `${window.location.protocol}//${window.location.host}/api`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateURL = GlobalStateStore.getState().session.apiURL?.trim();
|
||||||
|
if (stateURL) return normalizeApiURL(stateURL);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export { GlobalStateStore };
|
||||||
|
|
||||||
|
export function ensureApiURL(envURL?: string): string {
|
||||||
|
const resolved = resolveApiURL(envURL);
|
||||||
|
if (!resolved) return '';
|
||||||
|
|
||||||
|
const state = GlobalStateStore.getState();
|
||||||
|
if (state.session.apiURL !== resolved) {
|
||||||
|
state.setApiURL(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCurrentPath(pathname: string): void {
|
||||||
|
const state = GlobalStateStore.getState();
|
||||||
|
const current = state.navigation.currentPage ?? {};
|
||||||
|
|
||||||
|
state.setCurrentPage({
|
||||||
|
...current,
|
||||||
|
path: pathname
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user