258 lines
9.0 KiB
Svelte
258 lines
9.0 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from "svelte";
|
||
|
||
type AccessEntry = {
|
||
key_id: string;
|
||
last_accessed_at: string;
|
||
last_path: string;
|
||
request_count: number;
|
||
};
|
||
|
||
type StatusResponse = {
|
||
title: string;
|
||
description: string;
|
||
version: string;
|
||
build_date: string;
|
||
commit: string;
|
||
connected_count: number;
|
||
total_known: number;
|
||
connected_window: string;
|
||
oauth_enabled: boolean;
|
||
entries: AccessEntry[];
|
||
};
|
||
|
||
let data: StatusResponse | null = null;
|
||
let loading = true;
|
||
let error = "";
|
||
|
||
const quickLinks = [
|
||
{ href: "/llm", label: "LLM Instructions" },
|
||
{ href: "/healthz", label: "Health Check" },
|
||
{ href: "/readyz", label: "Readiness Check" },
|
||
];
|
||
|
||
async function loadStatus() {
|
||
loading = true;
|
||
error = "";
|
||
|
||
try {
|
||
const response = await fetch("/api/status");
|
||
if (!response.ok) {
|
||
throw new Error(`Status request failed with ${response.status}`);
|
||
}
|
||
data = (await response.json()) as StatusResponse;
|
||
} catch (err) {
|
||
error = err instanceof Error ? err.message : "Failed to load status";
|
||
} finally {
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
function formatDate(value: string) {
|
||
return new Date(value).toLocaleString();
|
||
}
|
||
|
||
onMount(loadStatus);
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>AMCS</title>
|
||
</svelte:head>
|
||
|
||
<div class="min-h-screen bg-slate-950 text-slate-100">
|
||
<main
|
||
class="mx-auto flex min-h-screen max-w-7xl flex-col px-4 py-6 sm:px-6 lg:px-8"
|
||
>
|
||
<section
|
||
class="overflow-hidden rounded-3xl border border-white/10 bg-slate-900 shadow-2xl shadow-slate-950/40"
|
||
>
|
||
<img
|
||
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 class="flex flex-wrap gap-3">
|
||
{#each quickLinks as link}
|
||
<a
|
||
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>
|
||
</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-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 && 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">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 font-semibold text-white"
|
||
>{entry.request_count}</td
|
||
>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</section>
|
||
</main>
|
||
</div>
|