feat: add dark mode, telemetry, and curl to Docker image
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 44s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m10s

- Install `curl` in Dockerfile for healthcheck/tooling support
- Add class-based dark mode via Tailwind `@custom-variant` and a
  pre-paint `<script>` in `app.html` to avoid theme flash on load
- Implement theme toggle in layout with system preference detection,
  `localStorage` persistence, and smooth transitions
- Update all UI components with `dark:` variants for full dark mode
  support across backgrounds, borders, and text colours
- Add `sendTelemetry` helper in `api.ts` for fire-and-forget
  client-side error reporting to `/api/v1/telemetry`
This commit is contained in:
2026-04-16 06:31:13 -04:00
parent 18710515d8
commit eb461f40ee
13 changed files with 304 additions and 109 deletions

View File

@@ -1,5 +1,8 @@
@import "tailwindcss";
/* Class-based dark mode: toggled via .dark on <html>. */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-pitch: #16a34a;
--color-pitch-dark: #15803d;

View File

@@ -5,6 +5,16 @@
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nike — Football Data Platform</title>
<!-- Apply theme before first paint to prevent flash of wrong theme. -->
<script>
try {
const stored = localStorage.getItem('nike-theme');
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (stored !== 'light' && systemDark)) {
document.documentElement.classList.add('dark');
}
} catch (_) {}
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View File

@@ -1,5 +1,25 @@
import type { LogsResponse, RunResult, StatusResponse } from './types';
interface TelemetryReport {
type: string;
message: string;
url?: string;
line?: number;
col?: number;
stack?: string;
}
/** Fire-and-forget client-side error report. Never throws. */
export function sendTelemetry(report: TelemetryReport): void {
fetch('/api/v1/telemetry', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(report),
}).catch(() => {
// telemetry is best-effort — swallow errors silently
});
}
export async function fetchStatus(): Promise<StatusResponse> {
const r = await fetch('/api/status');
if (!r.ok) throw new Error(`HTTP ${r.status}`);

View File

@@ -1,21 +1,91 @@
<script lang="ts">
import '../app.css';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { sendTelemetry } from '$lib/api';
let { children } = $props();
type ThemeOverride = 'dark' | 'light' | null;
let override = $state<ThemeOverride>(null);
let systemDark = $state(true);
// Derived: dark when explicitly set, otherwise follow system.
let isDark = $derived(override !== null ? override === 'dark' : systemDark);
// Keep <html> class in sync with isDark whenever it changes.
$effect(() => {
document.documentElement.classList.toggle('dark', isDark);
});
function toggleTheme() {
const next = !isDark;
// If toggling back to match system, remove the override.
if (next === systemDark) {
override = null;
localStorage.removeItem('nike-theme');
} else {
override = next ? 'dark' : 'light';
localStorage.setItem('nike-theme', override);
}
}
onMount(() => {
// ── Browser error telemetry ────────────────────────────
const onJsError = (e: ErrorEvent) => {
sendTelemetry({
type: 'js_error',
message: e.message,
url: e.filename || window.location.href,
line: e.lineno,
col: e.colno,
stack: e.error?.stack,
});
};
const onRejection = (e: PromiseRejectionEvent) => {
sendTelemetry({
type: 'unhandled_rejection',
message: String(e.reason),
url: window.location.href,
stack: e.reason?.stack,
});
};
window.addEventListener('error', onJsError);
window.addEventListener('unhandledrejection', onRejection);
// ── Theme initialisation ────────────────────────────────
const mq = window.matchMedia('(prefers-color-scheme: dark)');
systemDark = mq.matches;
const stored = localStorage.getItem('nike-theme');
if (stored === 'dark' || stored === 'light') {
override = stored as ThemeOverride;
}
const onSystemChange = (e: MediaQueryListEvent) => {
systemDark = e.matches;
};
mq.addEventListener('change', onSystemChange);
return () => {
window.removeEventListener('error', onJsError);
window.removeEventListener('unhandledrejection', onRejection);
mq.removeEventListener('change', onSystemChange);
};
});
const nav = [
{ href: '/', label: 'Status' },
{ href: '/tools', label: 'Tool Runner' },
];
</script>
<div class="min-h-screen bg-gray-950 text-gray-100">
<header class="border-b border-gray-800 bg-gray-900/80 backdrop-blur sticky top-0 z-10">
<div class="min-h-screen bg-slate-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100">
<header class="border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur sticky top-0 z-10">
<div class="mx-auto max-w-7xl px-4 py-3 flex items-center gap-6">
<div class="flex items-center gap-2">
<span class="text-green-500 text-lg leading-none"></span>
<span class="font-semibold text-white tracking-tight">Nike</span>
<span class="font-semibold text-gray-900 dark:text-white tracking-tight">Nike</span>
<span class="text-gray-500 text-sm hidden sm:inline">Football Data Platform</span>
</div>
<nav class="flex gap-1 ml-2">
@@ -25,12 +95,19 @@
class="px-3 py-1.5 rounded text-sm font-medium transition-colors {$page.url.pathname ===
item.href
? 'bg-green-700 text-white'
: 'text-gray-400 hover:text-white hover:bg-gray-800'}"
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'}"
>
{item.label}
</a>
{/each}
</nav>
<button
onclick={toggleTheme}
class="ml-auto text-xs px-2.5 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDark ? 'Light' : 'Dark'}
</button>
</div>
</header>

View File

@@ -83,16 +83,16 @@
<div class="space-y-5">
<!-- Title row -->
<div class="flex items-center justify-between">
<h1 class="text-lg font-semibold text-white">System Status</h1>
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">System Status</h1>
<div class="flex items-center gap-3">
{#if invalidateMsg}
<span class="text-sm text-green-400 transition-opacity">{invalidateMsg}</span>
<span class="text-sm text-green-600 dark:text-green-400 transition-opacity">{invalidateMsg}</span>
{/if}
<button
onclick={doInvalidate}
disabled={invalidating}
class="px-3 py-1.5 rounded bg-gray-800 hover:bg-gray-700 text-sm text-gray-300
border border-gray-700 disabled:opacity-50 transition-colors"
class="px-3 py-1.5 rounded bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-sm text-gray-700 dark:text-gray-300
border border-gray-300 dark:border-gray-700 disabled:opacity-50 transition-colors"
>
{invalidating ? 'Clearing…' : 'Clear Cache'}
</button>
@@ -100,7 +100,7 @@
</div>
{#if loadError}
<div class="p-4 rounded-lg bg-red-950 border border-red-800 text-red-300 text-sm">
<div class="p-4 rounded-lg bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-300 text-sm">
{loadError}
</div>
{/if}
@@ -109,65 +109,65 @@
<!-- Status cards -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- Database -->
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4 space-y-3">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4 space-y-3">
<div class="flex items-center gap-2">
<span class="inline-block w-2.5 h-2.5 rounded-full {dot(status.database.connected)}"></span>
<span class="font-medium text-sm text-white">Database</span>
<span class="font-medium text-sm text-gray-900 dark:text-white">Database</span>
</div>
<dl class="text-sm space-y-1.5">
<div class="flex justify-between">
<dt class="text-gray-500">Host</dt>
<dd class="text-gray-200">{status.database.host ?? '—'}</dd>
<dd class="text-gray-700 dark:text-gray-200">{status.database.host ?? '—'}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">Latency</dt>
<dd class="text-gray-200">{fmtMs(status.database.latency_ms)}</dd>
<dd class="text-gray-700 dark:text-gray-200">{fmtMs(status.database.latency_ms)}</dd>
</div>
{#if status.database.version}
<div class="flex justify-between">
<dt class="text-gray-500">Version</dt>
<dd class="text-gray-400 text-xs font-mono truncate max-w-40">
<dd class="text-gray-500 dark:text-gray-400 text-xs font-mono truncate max-w-40">
{status.database.version}
</dd>
</div>
{/if}
{#if status.database.error}
<div class="text-red-400 text-xs pt-1">{status.database.error}</div>
<div class="text-red-500 dark:text-red-400 text-xs pt-1">{status.database.error}</div>
{/if}
</dl>
</div>
<!-- TheSportsDB -->
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4 space-y-3">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4 space-y-3">
<div class="flex items-center gap-2">
<span class="inline-block w-2.5 h-2.5 rounded-full {dot(status.api.connected)}"></span>
<span class="font-medium text-sm text-white">TheSportsDB</span>
<span class="font-medium text-sm text-gray-900 dark:text-white">TheSportsDB</span>
</div>
<dl class="text-sm space-y-1.5">
<div class="flex justify-between">
<dt class="text-gray-500">Latency</dt>
<dd class="text-gray-200">{fmtMs(status.api.latency_ms)}</dd>
<dd class="text-gray-700 dark:text-gray-200">{fmtMs(status.api.latency_ms)}</dd>
</div>
{#if status.api.backend}
<div class="flex justify-between">
<dt class="text-gray-500">Backend</dt>
<dd class="text-gray-200">{status.api.backend}</dd>
<dd class="text-gray-700 dark:text-gray-200">{status.api.backend}</dd>
</div>
{/if}
{#if status.api.error}
<div class="text-red-400 text-xs pt-1">{status.api.error}</div>
<div class="text-red-500 dark:text-red-400 text-xs pt-1">{status.api.error}</div>
{/if}
</dl>
</div>
<!-- MCP Server -->
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4 space-y-3">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4 space-y-3">
<div class="flex items-center gap-2">
<span class="inline-block w-2.5 h-2.5 rounded-full {dot(status.mcp.running)}"></span>
<span class="font-medium text-sm text-white">MCP Server</span>
<span class="font-medium text-sm text-gray-900 dark:text-white">MCP Server</span>
{#if status.mcp.premium}
<span
class="ml-auto text-xs px-1.5 py-0.5 rounded bg-amber-900/60 text-amber-300 border border-amber-800"
class="ml-auto text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/60 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800"
>
Premium
</span>
@@ -176,19 +176,19 @@
<dl class="text-sm space-y-1.5">
<div class="flex justify-between">
<dt class="text-gray-500">Transport</dt>
<dd class="text-gray-200">{status.mcp.transport}</dd>
<dd class="text-gray-700 dark:text-gray-200">{status.mcp.transport}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">Uptime</dt>
<dd class="text-gray-200">{status.mcp.uptime}</dd>
<dd class="text-gray-700 dark:text-gray-200">{status.mcp.uptime}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">Tools</dt>
<dd class="text-gray-200">{status.mcp.tool_count}</dd>
<dd class="text-gray-700 dark:text-gray-200">{status.mcp.tool_count}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-gray-500 shrink-0">Endpoint</dt>
<dd class="text-gray-400 text-xs font-mono truncate" title={status.mcp.endpoint}>
<dd class="text-gray-500 dark:text-gray-400 text-xs font-mono truncate" title={status.mcp.endpoint}>
{status.mcp.endpoint}
</dd>
</div>
@@ -199,7 +199,7 @@
<!-- Followed teams + MCP tools -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Followed teams -->
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4">
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
Followed Teams
</h2>
@@ -210,39 +210,39 @@
{#each status.data.followed as team}
<li class="flex items-center gap-2 text-sm">
<span class="text-green-500 text-xs"></span>
<span class="text-white">{team.team}</span>
<span class="text-gray-700">·</span>
<span class="text-gray-400">{team.league}</span>
<span class="text-gray-900 dark:text-white">{team.team}</span>
<span class="text-gray-300 dark:text-gray-700">·</span>
<span class="text-gray-500 dark:text-gray-400">{team.league}</span>
</li>
{/each}
</ul>
{/if}
{#if status.data.last_cache}
<p class="mt-3 text-xs text-gray-600">
<p class="mt-3 text-xs text-gray-500">
Last cache update: {relTime(status.data.last_cache)}
</p>
{/if}
</div>
<!-- MCP Tools -->
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4">
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">MCP Tools</h2>
<table class="w-full text-sm">
<tbody class="divide-y divide-gray-800/60">
<tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
{#each status.tools as tool}
<tr>
<td class="py-1.5 pr-3">
<code class="text-green-300 text-xs">{tool.name}</code>
<code class="text-green-600 dark:text-green-300 text-xs">{tool.name}</code>
{#if tool.premium}
<span
class="ml-1.5 text-xs px-1 rounded bg-amber-900/50 text-amber-400 border border-amber-800/50"
class="ml-1.5 text-xs px-1 rounded bg-amber-100 dark:bg-amber-900/50 text-amber-600 dark:text-amber-400 border border-amber-200 dark:border-amber-800/50"
title="Requires premium TheSportsDB key"
>
</span>
{/if}
</td>
<td class="py-1.5 text-gray-400">{tool.description}</td>
<td class="py-1.5 text-gray-500 dark:text-gray-400">{tool.description}</td>
</tr>
{/each}
</tbody>
@@ -252,7 +252,7 @@
<!-- DB table counts -->
{#if Object.keys(status.data.table_counts).length > 0}
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4">
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
Database Contents
</h2>
@@ -260,7 +260,7 @@
{#each Object.entries(status.data.table_counts) as [table, count]}
<div>
<div class="text-xs text-gray-500">{table}</div>
<div class="text-white font-mono text-sm mt-0.5">{count.toLocaleString()}</div>
<div class="text-gray-900 dark:text-white font-mono text-sm mt-0.5">{count.toLocaleString()}</div>
</div>
{/each}
</div>
@@ -271,12 +271,12 @@
{/if}
<!-- Request Log -->
<div class="rounded-lg bg-gray-900 border border-gray-800">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800">
<div
class="px-4 py-3 border-b border-gray-800 flex items-center justify-between"
class="px-4 py-3 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between"
>
<h2 class="text-sm font-medium text-white">Request Log</h2>
<span class="text-xs text-gray-600">{logs.length} entries · auto-refreshes every 5 s</span>
<h2 class="text-sm font-medium text-gray-900 dark:text-white">Request Log</h2>
<span class="text-xs text-gray-500">{logs.length} entries · auto-refreshes every 5 s</span>
</div>
{#if logs.length === 0}
<p class="px-4 py-5 text-gray-500 text-sm">No MCP requests yet.</p>
@@ -284,23 +284,23 @@
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-gray-600 border-b border-gray-800">
<tr class="text-left text-xs text-gray-500 dark:text-gray-600 border-b border-gray-200 dark:border-gray-800">
<th class="px-4 py-2 font-medium">Time</th>
<th class="px-4 py-2 font-medium">Tool</th>
<th class="px-4 py-2 font-medium">Args</th>
<th class="px-4 py-2 font-medium text-right">Duration</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800/40">
<tbody class="divide-y divide-gray-100/40 dark:divide-gray-800/40">
{#each logs as entry}
<tr class="hover:bg-gray-800/40 transition-colors">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors">
<td class="px-4 py-2 text-gray-500 whitespace-nowrap text-xs">
{relTime(entry.timestamp)}
</td>
<td class="px-4 py-2">
<code class="text-green-300 text-xs">{entry.tool}</code>
<code class="text-green-600 dark:text-green-300 text-xs">{entry.tool}</code>
</td>
<td class="px-4 py-2 text-gray-400 text-xs font-mono max-w-xs truncate">
<td class="px-4 py-2 text-gray-500 dark:text-gray-400 text-xs font-mono max-w-xs truncate">
{fmtArgs(entry.args)}
</td>
<td class="px-4 py-2 text-right text-gray-500 whitespace-nowrap text-xs">

View File

@@ -178,8 +178,8 @@
<div class="space-y-5">
<div>
<h1 class="text-lg font-semibold text-white">Tool Runner</h1>
<p class="text-sm text-gray-400 mt-1">
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">Tool Runner</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
Run MCP tools interactively and inspect raw API responses. Useful for spotting strange API
results.
</p>
@@ -189,7 +189,7 @@
<!-- Left: selector + form + result -->
<div class="lg:col-span-2 space-y-4">
<!-- Tool selector -->
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4">
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">Select Tool</h2>
<div class="flex flex-wrap gap-2">
{#each TOOLS as tool}
@@ -198,11 +198,11 @@
class="px-3 py-1.5 rounded text-sm font-medium transition-colors
{selectedTool.name === tool.name
? 'bg-green-700 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700 border border-gray-700'}"
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700'}"
>
{tool.name}
{#if tool.premium}
<span use:melt={$premTrigger} class="ml-1 text-amber-400 cursor-help"></span>
<span use:melt={$premTrigger} class="ml-1 text-amber-500 dark:text-amber-400 cursor-help"></span>
{/if}
</button>
{/each}
@@ -210,34 +210,34 @@
</div>
<!-- Parameter form -->
<div class="rounded-lg bg-gray-900 border border-gray-800 p-4 space-y-4">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4 space-y-4">
<div>
<div class="flex items-center gap-2">
<h2 class="text-sm font-semibold text-white font-mono">{selectedTool.name}</h2>
<h2 class="text-sm font-semibold text-gray-900 dark:text-white font-mono">{selectedTool.name}</h2>
{#if selectedTool.premium}
<span
class="text-xs px-1.5 py-0.5 rounded bg-amber-900/60 text-amber-300 border border-amber-800/50"
class="text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/60 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800/50"
>
Premium
</span>
{/if}
</div>
<p class="text-xs text-gray-400 mt-1">{selectedTool.description}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{selectedTool.description}</p>
</div>
{#if selectedTool.params.length > 0}
<div class="space-y-3">
{#each selectedTool.params as param}
<div>
<label class="block text-xs font-medium text-gray-400 mb-1.5" for={param.key}>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5" for={param.key}>
{param.label}
</label>
{#if param.type === 'select'}
<select
id={param.key}
bind:value={paramValues[param.key]}
class="w-full rounded-md bg-gray-800 border border-gray-700 px-3 py-2 text-sm
text-gray-100 focus:outline-none focus:border-green-600 focus:ring-1
class="w-full rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-3 py-2 text-sm
text-gray-900 dark:text-gray-100 focus:outline-none focus:border-green-600 focus:ring-1
focus:ring-green-600"
>
{#each param.options ?? [] as opt}
@@ -250,8 +250,8 @@
type={param.type === 'number' ? 'number' : param.type === 'date' ? 'date' : 'text'}
placeholder={param.placeholder ?? ''}
bind:value={paramValues[param.key]}
class="w-full rounded-md bg-gray-800 border border-gray-700 px-3 py-2 text-sm
text-gray-100 placeholder-gray-600 focus:outline-none focus:border-green-600
class="w-full rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 px-3 py-2 text-sm
text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-green-600
focus:ring-1 focus:ring-green-600"
/>
{/if}
@@ -280,26 +280,26 @@
<!-- Result -->
{#if result !== null || resultError !== null}
<div
class="rounded-lg bg-gray-900 border {resultError
? 'border-red-800'
: 'border-gray-800'}"
class="rounded-lg bg-white dark:bg-gray-900 border {resultError
? 'border-red-200 dark:border-red-800'
: 'border-gray-200 dark:border-gray-800'}"
>
<div
class="px-4 py-2.5 border-b {resultError
? 'border-red-800'
: 'border-gray-800'} flex items-center gap-2"
? 'border-red-200 dark:border-red-800'
: 'border-gray-200 dark:border-gray-800'} flex items-center gap-2"
>
<span class="text-xs font-medium {resultError ? 'text-red-400' : 'text-green-400'}">
<span class="text-xs font-medium {resultError ? 'text-red-500 dark:text-red-400' : 'text-green-600 dark:text-green-400'}">
{resultError ? 'Error' : 'Result'}
</span>
{#if result}
<span class="text-xs text-gray-600">
<span class="text-xs text-gray-500">
{result.split('\n').length} lines
</span>
{/if}
</div>
<pre
class="px-4 py-4 text-sm font-mono whitespace-pre-wrap text-gray-200 overflow-x-auto
class="px-4 py-4 text-sm font-mono whitespace-pre-wrap text-gray-700 dark:text-gray-200 overflow-x-auto
max-h-[520px] overflow-y-auto leading-relaxed"
>{resultError ?? result}</pre>
</div>
@@ -307,30 +307,30 @@
</div>
<!-- Right: session history -->
<div class="rounded-lg bg-gray-900 border border-gray-800 sticky top-20">
<div class="px-4 py-3 border-b border-gray-800">
<div class="rounded-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 sticky top-20">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-800">
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider">Session History</h2>
</div>
{#if history.length === 0}
<p class="px-4 py-5 text-sm text-gray-500">No queries yet.</p>
{:else}
<ul class="divide-y divide-gray-800 max-h-[600px] overflow-y-auto">
<ul class="divide-y divide-gray-100 dark:divide-gray-800 max-h-[600px] overflow-y-auto">
{#each history as entry}
<li>
<button
onclick={() => loadHistory(entry)}
class="w-full text-left px-4 py-3 hover:bg-gray-800/50 transition-colors"
class="w-full text-left px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
>
<div class="flex items-center justify-between gap-2">
<code class="text-xs {entry.ok ? 'text-green-300' : 'text-red-400'} truncate">
<code class="text-xs {entry.ok ? 'text-green-600 dark:text-green-300' : 'text-red-500 dark:text-red-400'} truncate">
{entry.tool}
</code>
<span class="text-xs text-gray-600 shrink-0">{fmtTime(entry.ts)}</span>
<span class="text-xs text-gray-500 shrink-0">{fmtTime(entry.ts)}</span>
</div>
{#each Object.entries(entry.args) as [k, v]}
{#if v}
<div class="text-xs text-gray-500 truncate mt-0.5">
{k}: <span class="text-gray-400">{v}</span>
{k}: <span class="text-gray-500 dark:text-gray-400">{v}</span>
</div>
{/if}
{/each}
@@ -350,7 +350,7 @@
{#if $premOpen}
<div
use:melt={$premContent}
class="z-50 rounded bg-gray-800 border border-gray-700 px-2.5 py-1.5 text-xs text-amber-300 shadow-lg"
class="z-50 rounded bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 px-2.5 py-1.5 text-xs text-amber-700 dark:text-amber-300 shadow-lg"
>
Requires a premium TheSportsDB key
</div>