feat: initialize Nike Dashboard with SvelteKit and TailwindCSS

- Add package.json for project dependencies and scripts
- Create app.css with TailwindCSS imports and theme variables
- Set up basic HTML structure in app.html
- Implement API functions in api.ts for fetching status, logs, and running tools
- Define TypeScript interfaces in types.ts for API responses and logs
- Create layout component with navigation and main content area
- Disable SSR and prerendering in layout.ts
- Build main status page with real-time updates and logs
- Develop tool runner page for executing MCP tools with parameters
- Add favicon.svg for branding
- Configure SvelteKit adapter for static site generation
- Set up TypeScript configuration for the project
- Configure Vite with TailwindCSS and API proxy settings
- Create Docker Compose file for containerized deployment
This commit is contained in:
2026-03-28 17:26:40 +00:00
parent ee8436d5b8
commit 482657492d
23 changed files with 3209 additions and 4 deletions

View File

@@ -0,0 +1,357 @@
<script lang="ts">
import { createTooltip, melt } from '@melt-ui/svelte';
import { runTool } from '$lib/api';
// ── Tool definitions ────────────────────────────────────
type ParamType = 'text' | 'number' | 'date' | 'select';
interface Param {
key: string;
label: string;
type: ParamType;
default: string;
options?: string[];
placeholder?: string;
}
interface ToolDef {
name: string;
description: string;
premium: boolean;
params: Param[];
}
const TOOLS: ToolDef[] = [
{
name: 'get_team_info',
description: 'Team profile: stadium, capacity, location, founded year, colors.',
premium: false,
params: [
{ key: 'team_name', label: 'Team Name', type: 'text', default: 'Toronto FC', placeholder: 'e.g. Arsenal, Toronto FC' },
],
},
{
name: 'get_roster',
description: 'Current squad grouped by position. Requires premium key for live data.',
premium: false,
params: [
{ key: 'team_name', label: 'Team Name', type: 'text', default: 'Toronto FC' },
],
},
{
name: 'get_player_info',
description: 'Player profile: position, nationality, DOB, team, status.',
premium: false,
params: [
{ key: 'player_name', label: 'Player Name', type: 'text', default: '', placeholder: 'e.g. Federico Bernardeschi' },
],
},
{
name: 'get_fixtures',
description: 'Recent results and upcoming matches for a team.',
premium: false,
params: [
{ key: 'team_name', label: 'Team Name', type: 'text', default: 'Toronto FC' },
{ key: 'status', label: 'Status Filter', type: 'select', default: 'all', options: ['all', 'upcoming', 'past'] },
],
},
{
name: 'get_standings',
description: 'Full league table with points, goal difference, and form.',
premium: false,
params: [
{ key: 'league', label: 'League', type: 'text', default: 'American Major League Soccer', placeholder: 'e.g. English Premier League' },
{ key: 'season', label: 'Season', type: 'text', default: '2026', placeholder: 'e.g. 2026 or 2025-2026' },
],
},
{
name: 'get_match_result',
description: 'Match result for a team on a specific date.',
premium: false,
params: [
{ key: 'team_name', label: 'Team Name', type: 'text', default: 'Toronto FC' },
{ key: 'match_date', label: 'Date', type: 'date', default: '' },
],
},
{
name: 'get_match_detail',
description: 'Deep match stats, lineup, and timeline. Requires a premium TheSportsDB key.',
premium: true,
params: [
{ key: 'event_id', label: 'Event ID', type: 'number', default: '', placeholder: 'Get from get_fixtures first' },
],
},
{
name: 'get_livescores',
description: 'Current live soccer scores worldwide. Requires a premium TheSportsDB key.',
premium: true,
params: [],
},
];
// ── State ────────────────────────────────────────────────
let selectedTool = $state<ToolDef>(TOOLS[0]);
let paramValues = $state<Record<string, string>>({});
let running = $state(false);
let result = $state<string | null>(null);
let resultError = $state<string | null>(null);
interface HistoryEntry {
tool: string;
args: Record<string, string>;
output: string;
ok: boolean;
ts: Date;
}
let history = $state<HistoryEntry[]>([]);
function selectTool(tool: ToolDef) {
selectedTool = tool;
result = null;
resultError = null;
paramValues = Object.fromEntries(tool.params.map((p) => [p.key, p.default]));
}
// Init
selectTool(TOOLS[0]);
async function submit() {
running = true;
result = null;
resultError = null;
const args: Record<string, unknown> = {};
for (const p of selectedTool.params) {
const val = paramValues[p.key];
if (val === '') continue;
args[p.key] = p.type === 'number' ? Number(val) : val;
}
const snapshot = { ...paramValues };
try {
const r = await runTool(selectedTool.name, args);
if (r.ok) {
result = r.result ?? '';
} else {
resultError = r.error ?? 'Unknown error';
}
history = [
{ tool: selectedTool.name, args: snapshot, output: r.ok ? (r.result ?? '') : (r.error ?? ''), ok: r.ok, ts: new Date() },
...history,
].slice(0, 25);
} catch (e) {
resultError = String(e);
} finally {
running = false;
}
}
function loadHistory(entry: HistoryEntry) {
const tool = TOOLS.find((t) => t.name === entry.tool);
if (!tool) return;
selectTool(tool);
// selectTool resets paramValues, restore after microtask
setTimeout(() => {
paramValues = { ...entry.args };
if (entry.ok) {
result = entry.output;
resultError = null;
} else {
result = null;
resultError = entry.output;
}
}, 0);
}
function fmtTime(d: Date) {
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
// Melt UI tooltip for premium badge
const {
elements: { trigger: premTrigger, content: premContent },
states: { open: premOpen },
} = createTooltip({ positioning: { placement: 'top' }, openDelay: 200 });
</script>
<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">
Run MCP tools interactively and inspect raw API responses. Useful for spotting strange API
results.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start">
<!-- 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">
<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}
<button
onclick={() => selectTool(tool)}
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'}"
>
{tool.name}
{#if tool.premium}
<span use:melt={$premTrigger} class="ml-1 text-amber-400 cursor-help"></span>
{/if}
</button>
{/each}
</div>
</div>
<!-- Parameter form -->
<div class="rounded-lg bg-gray-900 border 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>
{#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"
>
Premium
</span>
{/if}
</div>
<p class="text-xs 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}>
{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
focus:ring-green-600"
>
{#each param.options ?? [] as opt}
<option value={opt}>{opt}</option>
{/each}
</select>
{:else}
<input
id={param.key}
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
focus:ring-1 focus:ring-green-600"
/>
{/if}
</div>
{/each}
</div>
{:else}
<p class="text-xs text-gray-500 italic">No parameters — just click Run.</p>
{/if}
<button
onclick={submit}
disabled={running}
class="px-4 py-2 rounded-md bg-green-700 hover:bg-green-600 text-white text-sm
font-medium disabled:opacity-50 transition-colors flex items-center gap-2"
>
{#if running}
<span class="inline-block w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin"></span>
Running…
{:else}
Run Tool
{/if}
</button>
</div>
<!-- Result -->
{#if result !== null || resultError !== null}
<div
class="rounded-lg bg-gray-900 border {resultError
? 'border-red-800'
: 'border-gray-800'}"
>
<div
class="px-4 py-2.5 border-b {resultError
? 'border-red-800'
: 'border-gray-800'} flex items-center gap-2"
>
<span class="text-xs font-medium {resultError ? 'text-red-400' : 'text-green-400'}">
{resultError ? 'Error' : 'Result'}
</span>
{#if result}
<span class="text-xs text-gray-600">
{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
max-h-[520px] overflow-y-auto leading-relaxed"
>{resultError ?? result}</pre>
</div>
{/if}
</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">
<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">
{#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"
>
<div class="flex items-center justify-between gap-2">
<code class="text-xs {entry.ok ? 'text-green-300' : 'text-red-400'} truncate">
{entry.tool}
</code>
<span class="text-xs text-gray-600 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>
</div>
{/if}
{/each}
{#if !entry.ok}
<div class="text-xs text-red-500 mt-0.5 truncate">{entry.output}</div>
{/if}
</button>
</li>
{/each}
</ul>
{/if}
</div>
</div>
</div>
<!-- Melt UI tooltip for premium star -->
{#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"
>
Requires a premium TheSportsDB key
</div>
{/if}