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)
This commit is contained in:
144
dashboard/package-lock.json
generated
144
dashboard/package-lock.json
generated
@@ -7,14 +7,12 @@
|
||||
"": {
|
||||
"name": "nike-dashboard",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@melt-ui/svelte": "^0.83.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"daisyui": "^5",
|
||||
"svelte": "^5.25.3",
|
||||
"svelte-check": "^4.1.4",
|
||||
"tailwindcss": "^4.1.3",
|
||||
@@ -438,40 +436,11 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.5",
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="
|
||||
},
|
||||
"node_modules/@internationalized/date": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz",
|
||||
"integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
@@ -481,6 +450,7 @@
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
@@ -490,6 +460,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
@@ -497,33 +468,19 @@
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@melt-ui/svelte": {
|
||||
"version": "0.83.0",
|
||||
"resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.83.0.tgz",
|
||||
"integrity": "sha512-E7QT+8YSftz+Hdk1W0hNR3f+cnaF2COMWkStn+2u4vk0RO1I9mXRJl+bJD6uhYaH146oxEB+5elu/ABbv6rpsA==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.3.1",
|
||||
"@floating-ui/dom": "^1.4.5",
|
||||
"@internationalized/date": "^3.5.0",
|
||||
"dequal": "^2.0.3",
|
||||
"focus-trap": "^7.5.2",
|
||||
"nanoid": "^5.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.118"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
@@ -865,6 +822,7 @@
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
|
||||
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
}
|
||||
@@ -957,14 +915,6 @@
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.20",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz",
|
||||
"integrity": "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
|
||||
@@ -1231,17 +1181,20 @@
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.57.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz",
|
||||
"integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -1254,6 +1207,7 @@
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1265,6 +1219,7 @@
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -1273,6 +1228,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -1296,6 +1252,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -1309,6 +1266,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.5.23",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.23.tgz",
|
||||
"integrity": "sha512-xuheNUSL4T6ZVtWXoioqcNkjoyGX85QTDz4HTw2aBPfqk4fuMjax5HDo8qCmpV6M1YN8bGvfx5BpYCoDeRlt+A==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -1335,14 +1301,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -1355,7 +1313,8 @@
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
|
||||
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="
|
||||
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.20.1",
|
||||
@@ -1414,12 +1373,14 @@
|
||||
"node_modules/esm-env": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz",
|
||||
"integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@typescript-eslint/types": "^8.2.0"
|
||||
@@ -1442,14 +1403,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/focus-trap": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz",
|
||||
"integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
|
||||
"dependencies": {
|
||||
"tabbable": "^6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1474,6 +1427,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.6"
|
||||
}
|
||||
@@ -1748,12 +1702,14 @@
|
||||
"node_modules/locate-character": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
@@ -1782,23 +1738,6 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz",
|
||||
"integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1965,6 +1904,7 @@
|
||||
"version": "5.55.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz",
|
||||
"integrity": "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -2010,11 +1950,6 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
|
||||
"integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
||||
@@ -2062,7 +1997,9 @@
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
@@ -2168,7 +2105,8 @@
|
||||
"node_modules/zimmerframe": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="
|
||||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"daisyui": "^5",
|
||||
"svelte": "^5.25.3",
|
||||
"svelte-check": "^4.1.4",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@melt-ui/svelte": "^0.83.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
@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;
|
||||
@plugin "daisyui" {
|
||||
themes: light --default, dark --prefersdark;
|
||||
}
|
||||
|
||||
/* Keep the "pitch" green identity as the primary color in both themes. */
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
--color-primary: #15803d;
|
||||
--color-primary-content: #ffffff;
|
||||
}
|
||||
@plugin "daisyui/theme" {
|
||||
name: "dark";
|
||||
prefersdark: true;
|
||||
--color-primary: #16a34a;
|
||||
--color-primary-content: #ffffff;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<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. -->
|
||||
<!-- Apply a manual theme override before first paint to prevent a flash.
|
||||
With no override, daisyUI follows the system via its prefersdark media query. -->
|
||||
<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');
|
||||
if (stored === 'dark' || stored === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', stored);
|
||||
}
|
||||
} catch (_) {}
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LogsResponse, RunResult, StatusResponse } from './types';
|
||||
import type { LogsResponse, RunResult, StatusResponse, ToolsResponse } from './types';
|
||||
|
||||
interface TelemetryReport {
|
||||
type: string;
|
||||
@@ -26,6 +26,12 @@ export async function fetchStatus(): Promise<StatusResponse> {
|
||||
return r.json();
|
||||
}
|
||||
|
||||
export async function fetchTools(): Promise<ToolsResponse> {
|
||||
const r = await fetch('/api/tools');
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
export async function fetchLogs(limit = 50): Promise<LogsResponse> {
|
||||
const r = await fetch(`/api/logs?limit=${limit}`);
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
|
||||
@@ -29,11 +29,22 @@ export interface DataStatus {
|
||||
followed: Array<{ team: string; league: string }>;
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
export interface ToolParam {
|
||||
name: string;
|
||||
type: string;
|
||||
default: string | number | null;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export interface ToolInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
readonly: boolean;
|
||||
premium?: boolean;
|
||||
premium: boolean;
|
||||
params: ToolParam[];
|
||||
}
|
||||
|
||||
export interface ToolsResponse {
|
||||
tools: ToolInfo[];
|
||||
}
|
||||
|
||||
export interface StatusResponse {
|
||||
@@ -41,7 +52,6 @@ export interface StatusResponse {
|
||||
api: ApiStatus;
|
||||
mcp: McpStatus;
|
||||
data: DataStatus;
|
||||
tools: Tool[];
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
|
||||
@@ -13,9 +13,13 @@
|
||||
// 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.
|
||||
// Apply a manual override via data-theme; clearing it lets daisyUI follow the system.
|
||||
$effect(() => {
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
if (override === null) {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', override);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleTheme() {
|
||||
@@ -80,22 +84,19 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<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="min-h-screen bg-base-200 text-base-content">
|
||||
<header class="border-b border-base-300 bg-base-100/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-gray-900 dark:text-white tracking-tight">Nike</span>
|
||||
<span class="text-gray-500 text-sm hidden sm:inline">Football Data Platform</span>
|
||||
<span class="text-primary text-lg leading-none">⚽</span>
|
||||
<span class="font-semibold tracking-tight">Nike</span>
|
||||
<span class="text-base-content/50 text-sm hidden sm:inline">Football Data Platform</span>
|
||||
</div>
|
||||
<nav class="flex gap-1 ml-2">
|
||||
{#each nav as item}
|
||||
<a
|
||||
href={item.href}
|
||||
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-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'}"
|
||||
class="btn btn-sm {$page.url.pathname === item.href ? 'btn-primary' : 'btn-ghost'}"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
@@ -103,7 +104,7 @@
|
||||
</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"
|
||||
class="btn btn-xs btn-outline rounded-full ml-auto"
|
||||
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{isDark ? 'Light' : 'Dark'}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { fetchLogs, fetchStatus, invalidateCache } from '$lib/api';
|
||||
import type { LogEntry, StatusResponse } from '$lib/types';
|
||||
import { fetchLogs, fetchStatus, fetchTools, invalidateCache } from '$lib/api';
|
||||
import type { LogEntry, StatusResponse, ToolInfo } from '$lib/types';
|
||||
|
||||
let status = $state<StatusResponse | null>(null);
|
||||
let tools = $state<ToolInfo[]>([]);
|
||||
let logs = $state<LogEntry[]>([]);
|
||||
let loadError = $state<string | null>(null);
|
||||
let invalidating = $state(false);
|
||||
@@ -48,6 +49,10 @@
|
||||
onMount(() => {
|
||||
loadStatus();
|
||||
loadLogs();
|
||||
// Tool catalogue is static for the process lifetime — fetch once.
|
||||
fetchTools()
|
||||
.then((r) => (tools = r.tools))
|
||||
.catch(() => {});
|
||||
statusTimer = setInterval(loadStatus, 30_000);
|
||||
logTimer = setInterval(loadLogs, 5_000);
|
||||
});
|
||||
@@ -57,10 +62,6 @@
|
||||
clearInterval(logTimer);
|
||||
});
|
||||
|
||||
function dot(connected: boolean | undefined) {
|
||||
return connected ? 'bg-green-500 shadow-green-500/50 shadow-sm' : 'bg-red-500';
|
||||
}
|
||||
|
||||
function fmtMs(ms: number | undefined | null) {
|
||||
if (ms == null) return '—';
|
||||
return `${ms.toFixed(0)} ms`;
|
||||
@@ -78,232 +79,237 @@
|
||||
const pairs = Object.entries(args).map(([k, v]) => `${k}=${JSON.stringify(v)}`);
|
||||
return pairs.join(', ') || '—';
|
||||
}
|
||||
|
||||
function firstLine(text: string) {
|
||||
return text.split('\n').map((l) => l.trim()).find((l) => l) ?? '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- Title row -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">System Status</h1>
|
||||
<h1 class="text-lg font-semibold">System Status</h1>
|
||||
<div class="flex items-center gap-3">
|
||||
{#if invalidateMsg}
|
||||
<span class="text-sm text-green-600 dark:text-green-400 transition-opacity">{invalidateMsg}</span>
|
||||
<span class="text-sm text-primary transition-opacity">{invalidateMsg}</span>
|
||||
{/if}
|
||||
<button
|
||||
onclick={doInvalidate}
|
||||
disabled={invalidating}
|
||||
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"
|
||||
>
|
||||
<button onclick={doInvalidate} disabled={invalidating} class="btn btn-sm btn-outline">
|
||||
{invalidating ? 'Clearing…' : 'Clear Cache'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loadError}
|
||||
<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>
|
||||
<div class="alert alert-error text-sm">{loadError}</div>
|
||||
{/if}
|
||||
|
||||
{#if status}
|
||||
<!-- Status cards -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<!-- Database -->
|
||||
<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-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-700 dark:text-gray-200">{status.database.host ?? '—'}</dd>
|
||||
<!-- Cache (PostgreSQL) -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="status {status.database.connected ? 'status-success' : 'status-error'}"></span>
|
||||
<span class="font-medium text-sm">Cache (PostgreSQL)</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">Latency</dt>
|
||||
<dd class="text-gray-700 dark:text-gray-200">{fmtMs(status.database.latency_ms)}</dd>
|
||||
</div>
|
||||
{#if status.database.version}
|
||||
<dl class="text-sm space-y-1.5">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">Version</dt>
|
||||
<dd class="text-gray-500 dark:text-gray-400 text-xs font-mono truncate max-w-40">
|
||||
{status.database.version}
|
||||
</dd>
|
||||
<dt class="text-base-content/60">Host</dt>
|
||||
<dd>{status.database.host ?? '—'}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if status.database.error}
|
||||
<div class="text-red-500 dark:text-red-400 text-xs pt-1">{status.database.error}</div>
|
||||
{/if}
|
||||
</dl>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-base-content/60">Latency</dt>
|
||||
<dd>{fmtMs(status.database.latency_ms)}</dd>
|
||||
</div>
|
||||
{#if status.database.version}
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-base-content/60">Version</dt>
|
||||
<dd class="text-base-content/60 text-xs font-mono truncate max-w-40">
|
||||
{status.database.version}
|
||||
</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if status.database.error}
|
||||
<div class="text-error text-xs pt-1">{status.database.error}</div>
|
||||
{/if}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TheSportsDB -->
|
||||
<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-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-700 dark:text-gray-200">{fmtMs(status.api.latency_ms)}</dd>
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="status {status.api.connected ? 'status-success' : 'status-error'}"></span>
|
||||
<span class="font-medium text-sm">TheSportsDB</span>
|
||||
</div>
|
||||
{#if status.api.backend}
|
||||
<dl class="text-sm space-y-1.5">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">Backend</dt>
|
||||
<dd class="text-gray-700 dark:text-gray-200">{status.api.backend}</dd>
|
||||
<dt class="text-base-content/60">Latency</dt>
|
||||
<dd>{fmtMs(status.api.latency_ms)}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if status.api.error}
|
||||
<div class="text-red-500 dark:text-red-400 text-xs pt-1">{status.api.error}</div>
|
||||
{/if}
|
||||
</dl>
|
||||
{#if status.api.backend}
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-base-content/60">Backend</dt>
|
||||
<dd>{status.api.backend}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if status.api.error}
|
||||
<div class="text-error text-xs pt-1">{status.api.error}</div>
|
||||
{/if}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP Server -->
|
||||
<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-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-100 dark:bg-amber-900/60 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800"
|
||||
>
|
||||
Premium
|
||||
</span>
|
||||
{/if}
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body p-4 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="status {status.mcp.running ? 'status-success' : 'status-error'}"></span>
|
||||
<span class="font-medium text-sm">MCP Server</span>
|
||||
{#if status.mcp.premium}
|
||||
<span class="badge badge-warning badge-sm ml-auto">Premium</span>
|
||||
{/if}
|
||||
</div>
|
||||
<dl class="text-sm space-y-1.5">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-base-content/60">Transport</dt>
|
||||
<dd>{status.mcp.transport}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-base-content/60">Uptime</dt>
|
||||
<dd>{status.mcp.uptime}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-base-content/60">Tools</dt>
|
||||
<dd>{status.mcp.tool_count}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-base-content/60 shrink-0">Endpoint</dt>
|
||||
<dd class="text-base-content/60 text-xs font-mono truncate" title={status.mcp.endpoint}>
|
||||
{status.mcp.endpoint}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<dl class="text-sm space-y-1.5">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-500">Transport</dt>
|
||||
<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-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-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-500 dark:text-gray-400 text-xs font-mono truncate" title={status.mcp.endpoint}>
|
||||
{status.mcp.endpoint}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Followed teams + MCP tools -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Followed teams -->
|
||||
<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>
|
||||
{#if status.data.followed.length === 0}
|
||||
<p class="text-gray-500 text-sm">No teams configured (set NIKE_TEAMS in .env)</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#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-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-500">
|
||||
Last cache update: {relTime(status.data.last_cache)}
|
||||
</p>
|
||||
{/if}
|
||||
<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">
|
||||
Followed Teams
|
||||
</h2>
|
||||
{#if status.data.followed.length === 0}
|
||||
<p class="text-base-content/60 text-sm">No teams configured (set NIKE_TEAMS in .env)</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#each status.data.followed as team}
|
||||
<li class="flex items-center gap-2 text-sm">
|
||||
<span class="text-primary text-xs">⚽</span>
|
||||
<span>{team.team}</span>
|
||||
<span class="text-base-content/30">·</span>
|
||||
<span class="text-base-content/60">{team.league}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if status.data.last_cache}
|
||||
<p class="mt-3 text-xs text-base-content/60">
|
||||
Last cache update: {relTime(status.data.last_cache)}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP Tools -->
|
||||
<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-100 dark:divide-gray-800/60">
|
||||
{#each status.tools as tool}
|
||||
<tr>
|
||||
<td class="py-1.5 pr-3">
|
||||
<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-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-500 dark:text-gray-400">{tool.description}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<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">MCP Tools</h2>
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
{#each tools as tool}
|
||||
<tr>
|
||||
<td class="align-top">
|
||||
<code class="text-primary text-xs">{tool.name}</code>
|
||||
{#if tool.premium}
|
||||
<span
|
||||
class="badge badge-warning badge-xs ml-1 tooltip"
|
||||
data-tip="Requires a premium TheSportsDB key"
|
||||
>
|
||||
★
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-base-content/60">{firstLine(tool.description)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DB table counts -->
|
||||
{#if Object.keys(status.data.table_counts).length > 0}
|
||||
<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>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{#each Object.entries(status.data.table_counts) as [table, count]}
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">{table}</div>
|
||||
<div class="text-gray-900 dark:text-white font-mono text-sm mt-0.5">{count.toLocaleString()}</div>
|
||||
</div>
|
||||
{/each}
|
||||
<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">
|
||||
Cache Contents
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{#each Object.entries(status.data.table_counts) as [table, count]}
|
||||
<div class="stat p-0">
|
||||
<div class="stat-title text-xs">{table}</div>
|
||||
<div class="stat-value text-base font-mono">{count.toLocaleString()}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if !loadError}
|
||||
<div class="text-gray-500 text-sm animate-pulse">Loading status…</div>
|
||||
<div class="flex items-center gap-2 text-base-content/60 text-sm">
|
||||
<span class="loading loading-spinner loading-sm"></span> Loading status…
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Request Log -->
|
||||
<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-200 dark:border-gray-800 flex items-center justify-between"
|
||||
>
|
||||
<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 class="card bg-base-100 border border-base-300">
|
||||
<div class="px-4 py-3 border-b border-base-300 flex items-center justify-between">
|
||||
<h2 class="text-sm font-medium">Request Log</h2>
|
||||
<span class="text-xs text-base-content/60">{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>
|
||||
<p class="px-4 py-5 text-base-content/60 text-sm">No MCP requests yet.</p>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<table class="table table-zebra table-sm">
|
||||
<thead>
|
||||
<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>
|
||||
<th>Time</th>
|
||||
<th>Tool</th>
|
||||
<th>Args</th>
|
||||
<th class="text-right">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100/40 dark:divide-gray-800/40">
|
||||
<tbody>
|
||||
{#each logs as entry}
|
||||
<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">
|
||||
<tr>
|
||||
<td class="text-base-content/60 whitespace-nowrap text-xs">
|
||||
{relTime(entry.timestamp)}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<code class="text-green-600 dark:text-green-300 text-xs">{entry.tool}</code>
|
||||
<td>
|
||||
<code class="text-primary text-xs">{entry.tool}</code>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-500 dark:text-gray-400 text-xs font-mono max-w-xs truncate">
|
||||
<td class="text-base-content/60 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">
|
||||
<td class="text-right text-base-content/60 whitespace-nowrap text-xs">
|
||||
{entry.duration_ms} ms
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,98 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { createTooltip, melt } from '@melt-ui/svelte';
|
||||
import { runTool } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import { fetchTools, runTool } from '$lib/api';
|
||||
import type { ToolInfo, ToolParam } from '$lib/types';
|
||||
|
||||
// ── Tool definitions ────────────────────────────────────
|
||||
|
||||
type ParamType = 'text' | 'number' | 'date' | 'select';
|
||||
|
||||
interface Param {
|
||||
key: string;
|
||||
label: string;
|
||||
type: ParamType;
|
||||
default: string;
|
||||
// ── 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' },
|
||||
};
|
||||
|
||||
interface ToolDef {
|
||||
name: string;
|
||||
description: string;
|
||||
premium: boolean;
|
||||
params: Param[];
|
||||
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 ?? '';
|
||||
}
|
||||
|
||||
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 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);
|
||||
@@ -107,26 +58,30 @@
|
||||
}
|
||||
let history = $state<HistoryEntry[]>([]);
|
||||
|
||||
function selectTool(tool: ToolDef) {
|
||||
function selectTool(tool: ToolInfo) {
|
||||
selectedTool = tool;
|
||||
result = null;
|
||||
resultError = null;
|
||||
paramValues = Object.fromEntries(tool.params.map((p) => [p.key, p.default]));
|
||||
paramValues = Object.fromEntries(tool.params.map((p) => [p.name, initialValue(p)]));
|
||||
}
|
||||
|
||||
// Init
|
||||
selectTool(TOOLS[0]);
|
||||
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.key];
|
||||
const val = paramValues[p.name];
|
||||
if (val === '') continue;
|
||||
args[p.key] = p.type === 'number' ? Number(val) : val;
|
||||
args[p.name] = p.type === 'integer' ? Number(val) : val;
|
||||
}
|
||||
|
||||
const snapshot = { ...paramValues };
|
||||
@@ -149,7 +104,7 @@
|
||||
}
|
||||
|
||||
function loadHistory(entry: HistoryEntry) {
|
||||
const tool = TOOLS.find((t) => t.name === entry.tool);
|
||||
const tool = tools.find((t) => t.name === entry.tool);
|
||||
if (!tool) return;
|
||||
selectTool(tool);
|
||||
// selectTool resets paramValues, restore after microtask
|
||||
@@ -168,190 +123,167 @@
|
||||
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-gray-900 dark:text-white">Tool Runner</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
<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>
|
||||
|
||||
<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-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}
|
||||
<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-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-500 dark:text-amber-400 cursor-help">★</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameter form -->
|
||||
<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-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-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}
|
||||
{#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>
|
||||
<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-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-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}
|
||||
<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-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"
|
||||
/>
|
||||
<!-- 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>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs text-gray-500 italic">No parameters — just click Run.</p>
|
||||
{/if}
|
||||
<p class="text-xs text-base-content/60 mt-1 whitespace-pre-line">{selectedTool.description}</p>
|
||||
</div>
|
||||
|
||||
<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-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-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-500 dark:text-red-400' : 'text-green-600 dark:text-green-400'}">
|
||||
{resultError ? 'Error' : 'Result'}
|
||||
</span>
|
||||
{#if result}
|
||||
<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-700 dark: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-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-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-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-600 dark:text-green-300' : 'text-red-500 dark:text-red-400'} truncate">
|
||||
{entry.tool}
|
||||
</code>
|
||||
<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-500 dark:text-gray-400">{v}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#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}
|
||||
{#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>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs text-base-content/60 italic">No parameters — just click Run.</p>
|
||||
{/if}
|
||||
|
||||
<!-- Melt UI tooltip for premium star -->
|
||||
{#if $premOpen}
|
||||
<div
|
||||
use:melt={$premContent}
|
||||
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>
|
||||
{/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>
|
||||
|
||||
Reference in New Issue
Block a user