feat: add broken tools page at /tool/broken
Create dedicated page to display all tools with failed health checks. Features: - Lists all tools with importHealth='BROKEN' OR executionHealth='BROKEN' - Displays health status badges for both import and execution - Shows error messages in code blocks for debugging - Includes last checked timestamp - Links to tool detail pages for manual recheck - Shows empty state with checkmark when all tools are healthy - Warning banner showing total broken tool count UI Components: - Card layout with red borders for broken tools - Health status icons (check/x) for visual status - Category badges and version info - Direct links to tool detail pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e5ba9fa441
commit
787410a7e0
1 changed files with 265 additions and 0 deletions
265
apps/web/src/app/tool/broken/page.tsx
Normal file
265
apps/web/src/app/tool/broken/page.tsx
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@tpmjs/ui/Card/Card';
|
||||
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
|
||||
import { Container } from '@tpmjs/ui/Container/Container';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AppHeader } from '~/components/AppHeader';
|
||||
|
||||
interface BrokenTool {
|
||||
id: string;
|
||||
exportName: string;
|
||||
description: string;
|
||||
importHealth: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
|
||||
executionHealth: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
|
||||
healthCheckError: string | null;
|
||||
lastHealthCheck: string | null;
|
||||
package: {
|
||||
npmPackageName: string;
|
||||
npmVersion: string;
|
||||
category: string;
|
||||
isOfficial: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Broken Tools Page
|
||||
*
|
||||
* Displays all tools with broken health status (importHealth='BROKEN' OR executionHealth='BROKEN')
|
||||
*/
|
||||
export default function BrokenToolsPage(): React.ReactElement {
|
||||
const [tools, setTools] = useState<BrokenTool[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch broken tools from API
|
||||
useEffect(() => {
|
||||
const fetchBrokenTools = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/tools/broken');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setTools(data.data);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(data.error || 'Failed to fetch broken tools');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBrokenTools();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<AppHeader />
|
||||
|
||||
{/* Main content */}
|
||||
<Container size="xl" padding="md" className="py-8">
|
||||
{/* Page header */}
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-3xl">⚠️</span>
|
||||
<h1 className="text-4xl font-bold text-foreground">Broken Tools</h1>
|
||||
</div>
|
||||
<p className="text-lg text-foreground-secondary">
|
||||
Tools that are currently failing health checks. These tools may not work correctly until
|
||||
the underlying issues are resolved.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Loading state */}
|
||||
{loading && (
|
||||
<div className="text-center py-12 text-foreground-secondary">Loading broken tools...</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{error && <div className="text-center py-12 text-red-500">Error: {error}</div>}
|
||||
|
||||
{/* Empty state - All healthy! */}
|
||||
{!loading && !error && tools.length === 0 && (
|
||||
<Card className="text-center py-12">
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<Icon icon="check" size="lg" className="text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2">All Tools are Healthy!</h2>
|
||||
<p className="text-foreground-secondary">
|
||||
No broken tools detected. All tools are passing health checks.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/tool/tool-search">
|
||||
<Button variant="default" size="md">
|
||||
Browse Tools
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Broken tools grid */}
|
||||
{!loading && !error && tools.length > 0 && (
|
||||
<>
|
||||
<div className="mb-6 p-4 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-xl mt-0.5">⚠️</span>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-red-800 dark:text-red-300 mb-1">
|
||||
{tools.length} {tools.length === 1 ? 'tool is' : 'tools are'} currently broken
|
||||
</h3>
|
||||
<p className="text-sm text-red-700 dark:text-red-400">
|
||||
These tools are experiencing import or execution failures. Click on a tool to
|
||||
see details and manually trigger a health recheck.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{tools.map((tool) => {
|
||||
const toolUrl = `/tool/${tool.package.npmPackageName}/${tool.exportName}`;
|
||||
const lastCheckedDate = tool.lastHealthCheck
|
||||
? new Date(tool.lastHealthCheck)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Card key={tool.id} className="flex flex-col border-red-200 dark:border-red-900">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<CardTitle>
|
||||
{tool.exportName !== 'default'
|
||||
? tool.exportName
|
||||
: tool.package.npmPackageName}
|
||||
</CardTitle>
|
||||
<div className="text-sm text-foreground-secondary mt-1">
|
||||
{tool.package.npmPackageName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription>{tool.description}</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 space-y-4">
|
||||
{/* Category badge and version */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary" size="sm">
|
||||
{tool.package.category}
|
||||
</Badge>
|
||||
<span className="text-xs text-foreground-tertiary">
|
||||
v{tool.package.npmVersion}
|
||||
</span>
|
||||
{tool.package.isOfficial && (
|
||||
<Badge variant="default" size="sm">
|
||||
Official
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Health status badges */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-foreground-secondary mb-2">
|
||||
Health Status:
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Import health */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-foreground-tertiary w-20">Import:</span>
|
||||
{tool.importHealth === 'BROKEN' ? (
|
||||
<Badge variant="error" size="sm">
|
||||
<Icon icon="x" size="sm" className="mr-1" />
|
||||
Broken
|
||||
</Badge>
|
||||
) : tool.importHealth === 'HEALTHY' ? (
|
||||
<Badge variant="success" size="sm">
|
||||
<Icon icon="check" size="sm" className="mr-1" />
|
||||
Healthy
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" size="sm">
|
||||
Unknown
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Execution health */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-foreground-tertiary w-20">
|
||||
Execution:
|
||||
</span>
|
||||
{tool.executionHealth === 'BROKEN' ? (
|
||||
<Badge variant="error" size="sm">
|
||||
<Icon icon="x" size="sm" className="mr-1" />
|
||||
Broken
|
||||
</Badge>
|
||||
) : tool.executionHealth === 'HEALTHY' ? (
|
||||
<Badge variant="success" size="sm">
|
||||
<Icon icon="check" size="sm" className="mr-1" />
|
||||
Healthy
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" size="sm">
|
||||
Unknown
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{tool.healthCheckError && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-foreground-secondary">
|
||||
Error:
|
||||
</div>
|
||||
<CodeBlock
|
||||
code={tool.healthCheckError}
|
||||
language="text"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Last checked timestamp */}
|
||||
{lastCheckedDate && (
|
||||
<div className="text-xs text-foreground-tertiary">
|
||||
Last checked: {lastCheckedDate.toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<Link href={toolUrl} className="w-full">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
View Details & Recheck →
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue