diff --git a/apps/web/src/app/tool/[...slug]/page.tsx b/apps/web/src/app/tool/[...slug]/page.tsx index 6e22922..04e8d8c 100644 --- a/apps/web/src/app/tool/[...slug]/page.tsx +++ b/apps/web/src/app/tool/[...slug]/page.tsx @@ -29,6 +29,10 @@ interface Tool { npmReadme: string | null; npmAuthor: { name: string; email?: string; url?: string } | string | null; npmMaintainers: Array<{ name: string; email?: string }> | null; + importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN'; + executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN'; + healthCheckError?: string | null; + lastHealthCheck?: string | null; tpmjsMetadata: { example?: string; parameters?: Array<{ @@ -77,6 +81,7 @@ export default function ToolDetailPage({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [slug, setSlug] = useState(''); + const [recheckLoading, setRecheckLoading] = useState(false); useEffect(() => { // Join slug array to reconstruct package name (e.g., ['@tpmjs', 'text-transformer'] -> '@tpmjs/text-transformer') @@ -137,6 +142,28 @@ export default function ToolDetailPage({ const authorName = typeof tool.npmAuthor === 'string' ? tool.npmAuthor : tool.npmAuthor?.name; + const recheckHealth = async () => { + setRecheckLoading(true); + try { + const response = await fetch(`/api/tools/${slug}`, { + method: 'POST', + }); + + if (!response.ok) { + const data = await response.json(); + alert(data.error || 'Recheck failed'); + return; + } + + // Refresh page to show updated health + window.location.reload(); + } catch { + alert('Failed to recheck health'); + } finally { + setRecheckLoading(false); + } + }; + return (
@@ -181,6 +208,56 @@ export default function ToolDetailPage({
+ {/* Health warning banner */} + {(tool.importHealth === 'BROKEN' || tool.executionHealth === 'BROKEN') && ( +
+
+ ⚠️ +
+

+ This tool is currently broken +

+
+ {tool.importHealth === 'BROKEN' && ( +
+ + Import Failed + + Cannot load from Railway service +
+ )} + {tool.executionHealth === 'BROKEN' && ( +
+ + Execution Failed + + Runtime error with test parameters +
+ )} +
+ {tool.healthCheckError && ( +
+                    {tool.healthCheckError}
+                  
+ )} + {tool.lastHealthCheck && ( +

+ Last checked: {new Date(tool.lastHealthCheck).toLocaleString()} +

+ )} + +
+
+
+ )} + {/* Main grid */}
{/* Left column - Main content */} diff --git a/apps/web/src/app/tool/tool-search/page.tsx b/apps/web/src/app/tool/tool-search/page.tsx index 7d4e266..fd20a4e 100644 --- a/apps/web/src/app/tool/tool-search/page.tsx +++ b/apps/web/src/app/tool/tool-search/page.tsx @@ -26,6 +26,8 @@ interface Tool { exportName: string; description: string; qualityScore: string; + importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN'; + executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN'; package: { npmPackageName: string; npmVersion: string; @@ -45,6 +47,7 @@ export default function ToolSearchPage(): React.ReactElement { const [activeTab, setActiveTab] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [categoryFilter, setCategoryFilter] = useState('all'); + const [healthFilter, setHealthFilter] = useState('all'); const [tools, setTools] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -69,6 +72,13 @@ export default function ToolSearchPage(): React.ReactElement { params.set('category', categoryFilter); } + if (healthFilter === 'healthy') { + params.set('importHealth', 'HEALTHY'); + params.set('executionHealth', 'HEALTHY'); + } else if (healthFilter === 'broken') { + params.set('broken', 'true'); + } + const response = await fetch(`/api/tools?${params.toString()}`); const data = await response.json(); @@ -98,7 +108,7 @@ export default function ToolSearchPage(): React.ReactElement { }; fetchTools(); - }, [searchQuery, activeTab, categoryFilter]); + }, [searchQuery, activeTab, categoryFilter, healthFilter]); return (
@@ -142,13 +152,29 @@ export default function ToolSearchPage(): React.ReactElement { />
+ {/* Health filter */} +
+ Health: +