feat(ui,api,admin): sveltekit admin spa, usage by day/agent/conversation/model, rate limits, memory tree, turn snapshot
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { AuditRecord } from "$lib/api/types";
|
||||
import EmptyState from "$lib/components/empty-state.svelte";
|
||||
import ErrorNote from "$lib/components/error-note.svelte";
|
||||
import PageHeader from "$lib/components/page-header.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Skeleton } from "$lib/components/ui/skeleton";
|
||||
import { fmtDateTime } from "$lib/format";
|
||||
import { session } from "$lib/session.svelte";
|
||||
|
||||
const PAGE = 100;
|
||||
let records = $state<AuditRecord[] | null>(null);
|
||||
let nextBefore = $state<number | null>(null);
|
||||
let failure = $state<string | null>(null);
|
||||
let busy = $state(false);
|
||||
|
||||
async function load(more = false) {
|
||||
const { client } = session;
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
failure = null;
|
||||
busy = true;
|
||||
try {
|
||||
const page = await client.audit({
|
||||
before: more && nextBefore ? nextBefore : undefined,
|
||||
limit: PAGE,
|
||||
});
|
||||
records = more ? [...(records ?? []), ...page.records] : page.records;
|
||||
nextBefore = page.next_before;
|
||||
} catch (cause) {
|
||||
failure = cause instanceof Error ? cause.message : String(cause);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
function detail(record: AuditRecord): string {
|
||||
if (typeof record.detail === "string") {
|
||||
return record.detail;
|
||||
}
|
||||
return Object.entries(record.detail)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
`${key}=${typeof value === "string" ? value : JSON.stringify(value)}`
|
||||
)
|
||||
.join(" ");
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Audit · Beaver</title></svelte:head>
|
||||
|
||||
<PageHeader title="Audit" />
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<div class="flex flex-col gap-3 px-4 py-4 sm:px-6">
|
||||
{#if failure}
|
||||
<ErrorNote message={failure} retry={() => load()} />
|
||||
{:else if records === null}
|
||||
<Skeleton class="h-40 w-full" />
|
||||
{:else if records.length === 0}
|
||||
<EmptyState
|
||||
hint="Logins, token changes and API writes land here."
|
||||
title="Nothing audited yet"
|
||||
/>
|
||||
{:else}
|
||||
<ul class="flex flex-col text-xs">
|
||||
{#each records as record (record.id)}
|
||||
<li
|
||||
class="ledger-grid grid-cols-[9rem_7rem_9rem_minmax(0,1fr)] border-b py-1.5"
|
||||
>
|
||||
<span class="tabular text-muted-foreground"
|
||||
>{fmtDateTime(record.ts)}</span
|
||||
>
|
||||
<span class="truncate font-medium">{record.kind}</span>
|
||||
<span class="truncate text-muted-foreground">{record.actor}</span>
|
||||
<span class="truncate font-mono text-muted-foreground">
|
||||
{#if record.agent}
|
||||
{record.agent}
|
||||
·
|
||||
{/if}
|
||||
{detail(record)}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if nextBefore !== null}
|
||||
<Button
|
||||
class="self-start"
|
||||
disabled={busy}
|
||||
onclick={() => load(true)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Older
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user