feat(playground): load all tools and add hide broken toggle

- Paginate through all tools from registry API (was limited to 20)
- Add 'Hide broken tools' checkbox (enabled by default)
- Filter out tools with BROKEN import or execution health
This commit is contained in:
Ajax Davis 2025-12-12 12:28:02 +10:00
parent aa1a5cd246
commit 4413ac00f6
2 changed files with 88 additions and 32 deletions

View file

@ -3,40 +3,74 @@ import { NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
interface RawTool {
id: string;
exportName: string;
description: string;
qualityScore: number;
importHealth: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
executionHealth: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
healthCheckError: string | null;
lastHealthCheck: string | null;
package?: {
npmPackageName: string;
npmVersion: string;
category: string;
frameworks: string[];
env: Array<{ name: string; description: string; required?: boolean; default?: string }>;
};
}
function transformTool(tool: RawTool) {
return {
toolId: tool.id,
packageName: tool.package?.npmPackageName,
exportName: tool.exportName,
description: tool.description,
category: tool.package?.category,
version: tool.package?.npmVersion,
qualityScore: tool.qualityScore,
frameworks: tool.package?.frameworks,
env: tool.package?.env,
importUrl: `https://esm.sh/${tool.package?.npmPackageName}@${tool.package?.npmVersion}`,
importHealth: tool.importHealth,
executionHealth: tool.executionHealth,
healthCheckError: tool.healthCheckError,
lastHealthCheck: tool.lastHealthCheck,
};
}
export async function GET() {
try {
const baseUrl = process.env.TPMJS_API_URL || 'https://tpmjs.com';
const response = await fetch(`${baseUrl}/api/tools`);
const allTools: RawTool[] = [];
let offset = 0;
const limit = 50; // Max allowed by the API
let hasMore = true;
if (!response.ok) {
throw new Error(`Failed to fetch tools: ${response.statusText}`);
// Paginate through all tools
while (hasMore) {
const response = await fetch(`${baseUrl}/api/tools?limit=${limit}&offset=${offset}`);
if (!response.ok) {
throw new Error(`Failed to fetch tools: ${response.statusText}`);
}
const data = await response.json();
const tools = data.data || [];
allTools.push(...tools);
hasMore = data.pagination?.hasMore ?? false;
offset += limit;
// Safety limit to prevent infinite loops
if (offset > 1000) break;
}
const data = await response.json();
// Transform web app response format to playground format
// Web app returns { success, data: Tool[] }
// Playground expects { success, tools: Tool[], total }
const tools = data.data || [];
return NextResponse.json({
success: data.success,
tools: tools.map((tool: any) => ({
toolId: tool.id,
packageName: tool.package?.npmPackageName,
exportName: tool.exportName,
description: tool.description,
category: tool.package?.category,
version: tool.package?.npmVersion,
qualityScore: tool.qualityScore,
frameworks: tool.package?.frameworks,
env: tool.package?.env,
importUrl: `https://esm.sh/${tool.package?.npmPackageName}@${tool.package?.npmVersion}`,
importHealth: tool.importHealth,
executionHealth: tool.executionHealth,
healthCheckError: tool.healthCheckError,
lastHealthCheck: tool.lastHealthCheck,
})),
total: tools.length,
success: true,
tools: allTools.map(transformTool),
total: allTools.length,
});
} catch (error) {
console.error('Failed to fetch tools:', error);

View file

@ -2,6 +2,7 @@
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
import { Checkbox } from '@tpmjs/ui/Checkbox/Checkbox';
import { Input } from '@tpmjs/ui/Input/Input';
import { ToolHealthBadge } from '@tpmjs/ui/ToolHealthBadge/ToolHealthBadge';
import { ToolHealthBanner } from '@tpmjs/ui/ToolHealthBanner/ToolHealthBanner';
@ -28,6 +29,7 @@ export function ToolsSidebar(): React.ReactElement {
const [tools, setTools] = useState<Tool[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
const [hideBroken, setHideBroken] = useState(true);
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
useEffect(() => {
@ -57,13 +59,20 @@ export function ToolsSidebar(): React.ReactElement {
fetchTools();
}, []);
const filteredTools = tools.filter(
(tool) =>
const filteredTools = tools.filter((tool) => {
// Filter by search text
const matchesFilter =
tool.packageName?.toLowerCase().includes(filter.toLowerCase()) ||
tool.exportName?.toLowerCase().includes(filter.toLowerCase()) ||
tool.description?.toLowerCase().includes(filter.toLowerCase()) ||
tool.category?.toLowerCase().includes(filter.toLowerCase())
);
tool.category?.toLowerCase().includes(filter.toLowerCase());
// Filter out broken tools if hideBroken is true
const isBroken = tool.importHealth === 'BROKEN' || tool.executionHealth === 'BROKEN';
const matchesHealth = !hideBroken || !isBroken;
return matchesFilter && matchesHealth;
});
return (
<>
@ -78,9 +87,22 @@ export function ToolsSidebar(): React.ReactElement {
placeholder="Filter tools..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="mb-4"
className="mb-3"
/>
<div className="mb-4 flex items-center gap-2 text-sm text-foreground-secondary">
<Checkbox
id="hide-broken"
checked={hideBroken}
onChange={(e) => setHideBroken(e.target.checked)}
size="sm"
/>
{/* biome-ignore lint/a11y/noLabelWithoutControl: Checkbox is associated via htmlFor */}
<label htmlFor="hide-broken" className="cursor-pointer">
Hide broken tools
</label>
</div>
<div className="flex-1 space-y-2 overflow-y-auto">
{loading ? (
<p className="text-sm text-foreground-secondary">Loading tools...</p>