From 493746b14c1251a45b061d2e3edd9160c929d2b9 Mon Sep 17 00:00:00 2001
From: benj <benj@rse8.com>
Date: Fri, 10 Apr 2026 11:13:34 +0800
Subject: a basic ui and landing web interface for tidyindex.com

---
 web/ui/src/routes/dashboard/keys/+page.svelte | 220 ++++++++++++++++++++++++++
 1 file changed, 220 insertions(+)
 create mode 100644 web/ui/src/routes/dashboard/keys/+page.svelte

(limited to 'web/ui/src/routes/dashboard/keys/+page.svelte')

diff --git a/web/ui/src/routes/dashboard/keys/+page.svelte b/web/ui/src/routes/dashboard/keys/+page.svelte
new file mode 100644
index 0000000..b0a066f
--- /dev/null
+++ b/web/ui/src/routes/dashboard/keys/+page.svelte
@@ -0,0 +1,220 @@
+<script lang="ts">
+  import { enhance } from '$app/forms';
+  import { DATASETS } from '$lib/datasets';
+  import { maskKey, scopeSummary } from '$lib/keys';
+  import { pushToast } from '$lib/stores/toasts';
+  import type { PageData, ActionData } from './$types';
+
+  let { data, form }: { data: PageData; form: ActionData } = $props();
+
+  let showCreate = $state(false);
+  let name = $state('');
+  let selected = $state<Set<string>>(new Set());
+  let allScopes = $state(true);
+
+  function toggleChip(slug: string) {
+    if (allScopes) {
+      allScopes = false;
+      selected = new Set([slug]);
+      return;
+    }
+    const next = new Set(selected);
+    if (next.has(slug)) next.delete(slug);
+    else next.add(slug);
+    if (next.size === 0) {
+      allScopes = true;
+      selected = new Set();
+    } else {
+      selected = next;
+    }
+  }
+
+  function selectAll() {
+    allScopes = true;
+    selected = new Set();
+  }
+
+  function fmtDate(ts: number): string {
+    return new Date(ts).toLocaleDateString('en-US', {
+      month: 'short',
+      day: 'numeric',
+      year: 'numeric'
+    });
+  }
+
+  async function copy(text: string) {
+    try {
+      await navigator.clipboard.writeText(text);
+      pushToast('Copied to clipboard', 'success');
+    } catch {
+      pushToast("Couldn't copy — select it manually", 'error');
+    }
+  }
+
+  // When a new key is returned in `form`, show toast + scroll to top.
+  $effect(() => {
+    if (form && 'created' in form && form.created) {
+      pushToast(`Key "${form.created.name}" created`, 'success');
+      showCreate = false;
+      name = '';
+      selected = new Set();
+      allScopes = true;
+    } else if (form && 'revokedId' in form && form.revokedId) {
+      pushToast('Key revoked', 'success');
+    } else if (form && 'error' in form && form.error) {
+      pushToast(form.error, 'error');
+    }
+  });
+</script>
+
+<p class="section-marker">§ 01 &nbsp;&middot;&nbsp; api keys</p>
+<div class="row-between mb-24">
+  <div>
+    <h1 class="page-title">Your API keys.</h1>
+    <p class="page-subtitle">
+      {data.activeCount} active, {data.keys.length - data.activeCount} revoked. Your
+      {data.plan.name} plan allows
+      {Number.isFinite(data.plan.maxKeys) ? data.plan.maxKeys : 'unlimited'}
+      active key{data.plan.maxKeys === 1 ? '' : 's'}.
+    </p>
+  </div>
+  {#if !showCreate}
+    <button class="btn btn-primary" onclick={() => (showCreate = true)}>
+      + New key
+    </button>
+  {/if}
+</div>
+
+{#if form && 'created' in form && form.created}
+  <div class="key-reveal">
+    <div class="key-reveal-head">
+      <span class="badge">New key</span>
+      <strong>Copy this key now — it won't be shown again.</strong>
+    </div>
+    <div class="key-reveal-value">
+      <span style="flex: 1;">{form.created.plaintext}</span>
+      <button class="btn btn-sm btn-ghost" onclick={() => copy(form.created!.plaintext)}>
+        Copy
+      </button>
+    </div>
+    <p class="key-reveal-warn">
+      Store it somewhere safe. Once you leave this page we only keep the hash.
+    </p>
+  </div>
+{/if}
+
+{#if showCreate}
+  <form
+    method="POST"
+    action="?/create"
+    class="card card-accent mb-24"
+    use:enhance
+  >
+    <div class="card-head">
+      <h2 class="card-title">New key</h2>
+      <button type="button" class="btn btn-sm btn-ghost" onclick={() => (showCreate = false)}>
+        Cancel
+      </button>
+    </div>
+
+    <div class="field">
+      <label class="field-label" for="name">name</label>
+      <input
+        id="name"
+        name="name"
+        class="input"
+        placeholder="e.g. production ingest"
+        bind:value={name}
+        required
+      />
+      <p class="help">Just for you — how you'll recognize this key in the list.</p>
+    </div>
+
+    <div class="field">
+      <span class="field-label">dataset scope</span>
+      <div class="chip-grid">
+        <button
+          type="button"
+          class="chip chip-all {allScopes ? 'active' : ''}"
+          onclick={selectAll}
+        >
+          all datasets
+        </button>
+        {#each DATASETS as slug}
+          <button
+            type="button"
+            class="chip {selected.has(slug) ? 'active' : ''}"
+            onclick={() => toggleChip(slug)}
+          >
+            {slug}
+          </button>
+        {/each}
+      </div>
+      <p class="help">
+        {#if allScopes}
+          This key will work against every dataset in the catalog.
+        {:else}
+          This key will only work against the {selected.size} selected dataset{selected.size === 1 ? '' : 's'}.
+        {/if}
+      </p>
+    </div>
+
+    {#if allScopes}
+      <input type="hidden" name="scopes" value="*" />
+    {:else}
+      {#each [...selected] as slug}
+        <input type="hidden" name="scopes" value={slug} />
+      {/each}
+    {/if}
+
+    <div class="row mt-16">
+      <button type="submit" class="btn btn-accent">Create key</button>
+      <button type="button" class="btn btn-ghost" onclick={() => (showCreate = false)}>
+        Cancel
+      </button>
+    </div>
+  </form>
+{/if}
+
+{#if data.keys.length === 0}
+  <div class="empty-state">
+    <p class="empty-title">No keys yet.</p>
+    <p>Click "+ New key" to create your first one.</p>
+  </div>
+{:else}
+  {#each data.keys as key (key.id)}
+    <div class="card">
+      <div class="key-row">
+        <div class="key-row-main">
+          <div class="key-name">
+            {key.name}
+            {#if key.active}
+              <span class="badge badge-success">Active</span>
+            {:else}
+              <span class="badge badge-muted">Revoked</span>
+            {/if}
+          </div>
+          <div class="key-mask">{maskKey(key.key_prefix)}</div>
+          <div class="scope-summary">
+            scope: <code>{scopeSummary(key.scopes)}</code>
+          </div>
+          <div class="key-meta">
+            <span><span class="meta-label">created</span> {fmtDate(key.created_at)}</span>
+            {#if key.last_used_at}
+              <span><span class="meta-label">last used</span> {fmtDate(key.last_used_at)}</span>
+            {:else}
+              <span><span class="meta-label">last used</span> never</span>
+            {/if}
+          </div>
+        </div>
+
+        {#if key.active}
+          <form method="POST" action="?/revoke" use:enhance>
+            <input type="hidden" name="id" value={key.id} />
+            <button class="btn btn-sm btn-danger" type="submit">Revoke</button>
+          </form>
+        {/if}
+      </div>
+    </div>
+  {/each}
+{/if}
-- 
cgit v1.2.3

