Files
nike/dashboard/src/routes/tools/+page.svelte
Robert Helewka 62af6727e6
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 42s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m20s
feat: migrate from RapidAPI to TheSportsDB with SvelteKit dashboard
- Replace free-api-live-football-data (RapidAPI) backend with TheSportsDB
- Add PostgreSQL cache layer for permanent data (teams, players, leagues, events)
- Replace Bootstrap dashboard with SvelteKit-based interactive dashboard
- Restructure MCP tools around TheSportsDB capabilities (get_team_info, get_roster, get_fixtures, get_standings, etc.)
- Expose tool registry via GET /api/tools so dashboard stays in sync
- Remove legacy modules and references (api_football, sync, RapidAPI env vars)
2026-06-11 10:22:24 -04:00

290 lines
9.1 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { fetchTools, runTool } from '$lib/api';
import type { ToolInfo, ToolParam } from '$lib/types';
// ── Frontend-only form niceties, keyed by backend param name ──
// The backend tool schema carries name/type/default/required; labels,
// placeholders, select options, and the input widget live here so the
// API stays presentation-free.
interface ParamUi {
label?: string;
input?: 'date' | 'select';
options?: string[];
placeholder?: string;
fallbackDefault?: string;
}
const PARAM_UI: Record<string, ParamUi> = {
team_name: { label: 'Team Name', placeholder: 'e.g. Arsenal, Toronto FC', fallbackDefault: 'Toronto FC' },
player_name: { label: 'Player Name', placeholder: 'e.g. Federico Bernardeschi' },
status: { label: 'Status Filter', input: 'select', options: ['all', 'upcoming', 'past'] },
match_date: { label: 'Date', input: 'date' },
event_id: { label: 'Event ID', placeholder: 'Get from get_fixtures first' },
league: { label: 'League', placeholder: 'e.g. English Premier League' },
season: { label: 'Season', placeholder: 'e.g. 2026 or 2025-2026' },
};
function uiFor(p: ToolParam): ParamUi {
return PARAM_UI[p.name] ?? {};
}
function labelFor(p: ToolParam) {
return uiFor(p).label ?? p.name;
}
function inputType(p: ToolParam): 'text' | 'number' | 'date' {
const ui = uiFor(p);
if (ui.input === 'date') return 'date';
return p.type === 'integer' ? 'number' : 'text';
}
function initialValue(p: ToolParam): string {
if (p.default != null) return String(p.default);
return uiFor(p).fallbackDefault ?? '';
}
// ── State ────────────────────────────────────────────────
let tools = $state<ToolInfo[]>([]);
let selectedTool = $state<ToolInfo | null>(null);
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: ToolInfo) {
selectedTool = tool;
result = null;
resultError = null;
paramValues = Object.fromEntries(tool.params.map((p) => [p.name, initialValue(p)]));
}
onMount(async () => {
const r = await fetchTools();
tools = r.tools;
if (tools.length > 0) selectTool(tools[0]);
});
async function submit() {
if (!selectedTool) return;
running = true;
result = null;
resultError = null;
const args: Record<string, unknown> = {};
for (const p of selectedTool.params) {
const val = paramValues[p.name];
if (val === '') continue;
args[p.name] = p.type === 'integer' ? 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' });
}
</script>
<div class="space-y-5">
<div>
<h1 class="text-lg font-semibold">Tool Runner</h1>
<p class="text-sm text-base-content/60 mt-1">
Run MCP tools interactively and inspect raw API responses. Useful for spotting strange API
results.
</p>
</div>
{#if !selectedTool}
<div class="flex items-center gap-2 text-base-content/60 text-sm">
<span class="loading loading-spinner loading-sm"></span> Loading tools…
</div>
{:else}
<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="card bg-base-100 border border-base-300">
<div class="card-body p-4">
<h2 class="text-xs font-medium text-base-content/60 uppercase tracking-wider mb-1">Select Tool</h2>
<div class="flex flex-wrap gap-2">
{#each tools as tool}
<button
onclick={() => selectTool(tool)}
class="btn btn-sm {selectedTool.name === tool.name ? 'btn-primary' : 'btn-soft'}"
>
{tool.name}
{#if tool.premium}
<span
class="tooltip cursor-help text-warning"
data-tip="Requires a premium TheSportsDB key"
>
</span>
{/if}
</button>
{/each}
</div>
</div>
</div>
<!-- Parameter form -->
<div class="card bg-base-100 border border-base-300">
<div class="card-body p-4 gap-4">
<div>
<div class="flex items-center gap-2">
<h2 class="text-sm font-semibold font-mono">{selectedTool.name}</h2>
{#if selectedTool.premium}
<span class="badge badge-warning badge-sm">Premium</span>
{/if}
</div>
<p class="text-xs text-base-content/60 mt-1 whitespace-pre-line">{selectedTool.description}</p>
</div>
{#if selectedTool.params.length > 0}
<div class="space-y-3">
{#each selectedTool.params as param}
<div>
<label class="label text-xs font-medium mb-1" for={param.name}>
{labelFor(param)}
</label>
{#if uiFor(param).input === 'select'}
<select
id={param.name}
bind:value={paramValues[param.name]}
class="select select-sm w-full"
>
{#each uiFor(param).options ?? [] as opt}
<option value={opt}>{opt}</option>
{/each}
</select>
{:else}
<input
id={param.name}
type={inputType(param)}
placeholder={uiFor(param).placeholder ?? ''}
bind:value={paramValues[param.name]}
class="input input-sm w-full"
/>
{/if}
</div>
{/each}
</div>
{:else}
<p class="text-xs text-base-content/60 italic">No parameters — just click Run.</p>
{/if}
<button onclick={submit} disabled={running} class="btn btn-primary btn-sm self-start">
{#if running}
<span class="loading loading-spinner loading-xs"></span>
Running…
{:else}
Run Tool
{/if}
</button>
</div>
</div>
<!-- Result -->
{#if result !== null || resultError !== null}
<div class="card bg-base-100 border {resultError ? 'border-error' : 'border-base-300'}">
<div
class="px-4 py-2.5 border-b {resultError ? 'border-error' : 'border-base-300'} flex items-center gap-2"
>
<span class="text-xs font-medium {resultError ? 'text-error' : 'text-success'}">
{resultError ? 'Error' : 'Result'}
</span>
{#if result}
<span class="text-xs text-base-content/60">
{result.split('\n').length} lines
</span>
{/if}
</div>
<pre
class="px-4 py-4 text-sm font-mono whitespace-pre-wrap overflow-x-auto
max-h-[520px] overflow-y-auto leading-relaxed"
>{resultError ?? result}</pre>
</div>
{/if}
</div>
<!-- Right: session history -->
<div class="card bg-base-100 border border-base-300 sticky top-20">
<div class="px-4 py-3 border-b border-base-300">
<h2 class="text-xs font-medium text-base-content/60 uppercase tracking-wider">Session History</h2>
</div>
{#if history.length === 0}
<p class="px-4 py-5 text-sm text-base-content/60">No queries yet.</p>
{:else}
<ul class="divide-y divide-base-300 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-base-200 transition-colors"
>
<div class="flex items-center justify-between gap-2">
<code class="text-xs {entry.ok ? 'text-primary' : 'text-error'} truncate">
{entry.tool}
</code>
<span class="text-xs text-base-content/60 shrink-0">{fmtTime(entry.ts)}</span>
</div>
{#each Object.entries(entry.args) as [k, v]}
{#if v}
<div class="text-xs text-base-content/60 truncate mt-0.5">
{k}: <span class="text-base-content/80">{v}</span>
</div>
{/if}
{/each}
{#if !entry.ok}
<div class="text-xs text-error mt-0.5 truncate">{entry.output}</div>
{/if}
</button>
</li>
{/each}
</ul>
{/if}
</div>
</div>
{/if}
</div>