feat(ui): Centralized PMOVES UI with real-time health monitoring - #375
Conversation
Implements TAC 1: Single branded dashboard showing all 94+ services with real-time health monitoring, eliminating URL jumping between services. Features: - Service catalog with 94+ services across 11 categories - Real-time health monitoring (30s auto-refresh) - Tier/category filtering on services dashboard - Expandable tier overview cards on landing page - System-wide health statistics bar - Search functionality for services New Components: - SystemStatsBar - Overall health percentage and service counts - TierNavigation - Category filter pills - TierOverview - Expandable tier summary cards - ServiceHealthIndicator - Visual health status dot - SystemHubSection - Landing page service hub New APIs: - GET /api/services - Service catalog with filtering - GET /api/health-all - Health check for all/specific services - GET /api/services-hub - Aggregated catalog + health endpoint New Hooks: - useServiceHealth - Client-side health polling with auto-refresh 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes 4 critical type errors identified by PR review: - Change isChecking → isPolling (useServiceHealth returns isPolling) - Fix ServiceCategory import from serviceCatalog instead of serviceHealth - Add 'use client' directive to services/page.tsx for hooks - Remove metadata export (incompatible with client components) - Fix ServiceEndpoint.url → endpoints.length check Resolves: - Type error: Property 'isChecking' does not exist - Type error: Module has no exported member 'ServiceCategory' - Type error: React hooks require client component 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add missing permissions blocks to 3 workflows and fix out-of-diff issues: - Add permissions: contents: read to chit-contract.yml, python-tests.yml, webhook-smoke.yml - Fix .gitmodules e2b submodule path (pmoves/pmoves/vendor/e2b → pmoves/vendor/e2b) - Add SCRIPTS variable to Makefile (was undefined) - Fix Makefile test-smoke targets (remove cd pmoves && for make -C compatibility) - Add standard Makefile targets (all, test, clean, .DEFAULT_GOAL) - Add learnings document 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updates Jest and E2E tests to work with the new SERVICE_CATALOG: - Changed heading check from "integration services" to "services" - Updated service list to use SERVICE_CATALOG entries - Separated catalog visibility tests from markdown doc tests Test results: - Jest: 10/11 passing (2 suites) - E2E: 4/4 passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR introduces a comprehensive service discovery and health monitoring system for the PMOVES UI, featuring a new service catalog with 94+ services, real-time health checking APIs, polling-based health monitoring hooks, and a redesigned services dashboard. It also adds multiple orchestration and testing targets to the Makefile, reorganizes a Git submodule path, and grants explicit read permissions to three GitHub Actions workflows. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant UIComponent as UI Component<br/>(ServicesDashboard)
participant Hook as useServiceHealth<br/>(Hook)
participant API as /api/services-hub<br/>(Next.js Route)
participant Catalog as SERVICE_CATALOG
participant HealthCheck as probeService<br/>(Health Prober)
Browser->>UIComponent: Mount dashboard
UIComponent->>Hook: Initialize useServiceHealth()
activate Hook
Hook->>API: Fetch /api/services-hub<br/>(initial)
API->>Catalog: Get all services
API->>HealthCheck: probeService (concurrent batch)
HealthCheck-->>API: ServiceHealth[]
API->>API: Calculate tier stats<br/>& critical services
API-->>Hook: HubData (services, health,<br/>tiers, stats)
Hook->>Hook: Build ServiceHealthMap
Hook-->>UIComponent: {health, status, lastUpdate}
UIComponent->>UIComponent: Render services grid<br/>with health indicators
Browser-->>UIComponent: Display dashboard
Note over Hook: Poll every 30s<br/>(default interval)
rect rgb(220, 240, 255)
Hook->>API: Fetch /api/services-hub<br/>(poll)
API->>HealthCheck: probeService (filtered)
HealthCheck-->>API: ServiceHealth[]
API-->>Hook: Updated HubData
Hook->>Hook: Update health map
Hook-->>UIComponent: Updated {health, lastUpdate}
UIComponent->>UIComponent: Re-render with new status
end
Browser->>UIComponent: User clicks Refresh
UIComponent->>Hook: Call refresh()
Hook->>API: Immediate fetch
API->>HealthCheck: probeService (all)
HealthCheck-->>API: ServiceHealth[]
API-->>Hook: HubData
Hook-->>UIComponent: {health, isChecking: false}
UIComponent->>UIComponent: Update dashboard
sequenceDiagram
participant ServiceGrid as ServiceGrid<br/>Component
participant HealthMap as healthMap prop<br/>(ServiceHealthMap)
participant ServiceCard as ServiceCard<br/>Component
participant Indicator as ServiceHealthIndicator<br/>Component
rect rgb(240, 245, 250)
Note over ServiceGrid,Indicator: Service List Rendering with Health Status
end
ServiceGrid->>ServiceGrid: Derive categories from services[]
ServiceGrid->>ServiceGrid: Initialize state:<br/>searchQuery, selectedCategory
ServiceGrid->>ServiceGrid: Render category filter buttons
Browser->>ServiceGrid: User searches or filters
ServiceGrid->>ServiceGrid: Update searchQuery state
ServiceGrid->>ServiceGrid: Filter services by title,<br/>summary, slug, capabilities
ServiceGrid->>ServiceCard: Map each service<br/>+ healthMap[slug]
activate ServiceCard
ServiceCard->>HealthMap: Lookup health status<br/>for service
HealthMap-->>ServiceCard: {status, responseTime}
ServiceCard->>ServiceCard: Select color theme<br/>from service.color
ServiceCard->>ServiceCard: Resolve primaryEndpoint
ServiceCard->>Indicator: Render health indicator
activate Indicator
Indicator->>Indicator: Apply status-based styling<br/>(healthy/unhealthy/unknown)
Indicator-->>ServiceCard: Rendered indicator
deactivate Indicator
ServiceCard->>ServiceCard: Render card with metadata,<br/>endpoints, health badge
ServiceCard-->>ServiceGrid: Rendered ServiceCard
deactivate ServiceCard
ServiceGrid->>ServiceGrid: Render grid of cards
Browser-->>ServiceGrid: Display filtered service list<br/>with health indicators
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes The PR introduces substantial new functionality spanning service discovery, health monitoring APIs, a comprehensive UI component suite, and orchestration tooling. Multiple concerns require attention: API logic density (tier stats, filtering, health aggregation), hook complexity (polling, lifecycle management, state updates), component interactions (health propagation through component tree), catalog size (94+ services with metadata), and cross-file integration patterns. While changes follow consistent patterns within cohorts, heterogeneity across workflows, Makefile, APIs, and UI components demands separate reasoning for each area. Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| endpoints: [ | ||
| { name: 'Web UI', port: '9090', path: '/', type: 'ui' }, | ||
| { name: 'API', port: '9090', path: '/api/v1', type: 'api' }, | ||
| { name: 'Health', port: '9090', path: '/-/ready', type: 'health' }, | ||
| ], | ||
| healthCheck: 'http://localhost:9090/-/ready', |
There was a problem hiding this comment.
Use container-reachable URLs for health checks
Because /api/services-hub calls checkAllServices() server-side, these healthCheck URLs are fetched from inside the pmoves-ui container. In Docker, localhost resolves to the UI container itself, not Prometheus/Grafana/etc., so these probes will consistently fail even when services are healthy. This makes the new health dashboard report false negatives when run via docker compose (the default stack). Consider using service DNS names (e.g., http://prometheus:9090/-/ready) or an env-based base URL that resolves from inside the UI container.
Useful? React with 👍 / 👎.
| slug: 'invidious', | ||
| title: 'Invidious', | ||
| summary: 'Privacy-focused YouTube frontend', | ||
| category: 'integration', | ||
| color: 'cyan', | ||
| endpoints: [ | ||
| { name: 'Web UI', port: '8095', path: '/', type: 'ui' }, | ||
| ], |
There was a problem hiding this comment.
Point Invidious to the configured host port
The catalog links Invidious to port 8095, but the docker-compose definition exposes Invidious on host port 3005 (127.0.0.1:3005:3000). As a result, the UI link for Invidious (and any derived health checks) will consistently fail for local deployments that follow the repo’s compose config. Update this entry to match the configured port so the dashboard points to a reachable endpoint.
Useful? React with 👍 / 👎.
| const criticalSlugs = [ | ||
| 'prometheus', // Observability foundation | ||
| 'postgres', // Primary database | ||
| 'nats', // Message bus | ||
| 'agent-zero', // Orchestrator | ||
| 'tensorzero', // LLM gateway | ||
| 'hi-rag-v2', // Knowledge retrieval |
There was a problem hiding this comment.
Track TensorZero gateway with the correct slug
The critical-down list checks for tensorzero, but the catalog defines the gateway as tensorzero-gateway. Because the slugs do not match, the hub will never flag the LLM gateway as critical even when it is down, which undermines the “critical services” alert. Use the catalog’s actual slug (tensorzero-gateway) or update the catalog to match.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (19)
pmoves/ui/components/services/ServicesHeaderActions.tsx (2)
27-32: Replace full page reload with Next.js router refresh.Using
window.location.reload()causes a full page reload, losing client-side state and providing a poor user experience. Consider using Next.js router methods instead.🔎 Proposed fix using Next.js router
'use client'; /* ═══════════════════════════════════════════════════════════════════════════ Services Header Actions Component Client Component for interactive header elements ═══════════════════════════════════════════════════════════════════════════ */ import React from 'react'; +import { useRouter } from 'next/navigation'; interface ServicesHeaderActionsProps { healthPercentage: number; isHealthy: boolean; } export function ServicesHeaderActions({ healthPercentage, isHealthy }: ServicesHeaderActionsProps) { + const router = useRouter(); + return ( <div className="flex items-center gap-3"> {/* Health indicator */} <div className="flex items-center gap-2"> <div className={`w-2 h-2 ${isHealthy ? 'bg-cata-forest animate-pulse' : 'bg-cata-ember'}`} /> <span className="font-pixel text-[6px] uppercase text-ink-muted"> {healthPercentage}% </span> </div> {/* Refresh button */} <button - onClick={() => window.location.reload()} + onClick={() => router.refresh()} + aria-label="Refresh service health status" className="tag tag-cyan hover:tag-cyan/80 transition-colors cursor-pointer" > Refresh </button> </div> ); }
19-24: Add accessibility attributes to health indicator.The health indicator lacks semantic information for screen readers. Consider adding ARIA attributes to improve accessibility.
🔎 Proposed accessibility improvements
{/* Health indicator */} - <div className="flex items-center gap-2"> + <div className="flex items-center gap-2" role="status" aria-live="polite"> <div className={`w-2 h-2 ${isHealthy ? 'bg-cata-forest animate-pulse' : 'bg-cata-ember'}`} /> <span className="font-pixel text-[6px] uppercase text-ink-muted"> - {healthPercentage}% + {healthPercentage}% healthy </span> + <span className="sr-only"> + System health: {isHealthy ? 'operational' : 'degraded'} + </span> </div>pmoves/ui/e2e/services.spec.ts (1)
26-28: Use escapeRegExp helper for consistent regex construction.While the static analysis warning about ReDoS is a false positive here (service.title is from a controlled list, not user input), it's better to use the existing
escapeRegExphelper consistently for all dynamic regex patterns.🔎 Proposed fix for consistent regex escaping
// Use .first() since new design has multiple links per service (card + quick links) for (const service of SERVICES) { - await expect(page.getByRole('link', { name: new RegExp(service.title, 'i') }).first()).toBeVisible(); + await expect(page.getByRole('link', { name: new RegExp(escapeRegExp(service.title), 'i') }).first()).toBeVisible(); }pmoves/ui/app/api/services-hub/route.ts (2)
130-130: Validate parseInt result to prevent NaN.The
parseIntcall doesn't validate the result, which could lead toNaNbeing passed tocheckAllServicesif an invalid timeout value is provided.🔎 Proposed fix
- const timeout = parseInt(searchParams.get('timeout') || '3000', 10); + const timeoutParam = parseInt(searchParams.get('timeout') || '3000', 10); + const timeout = isNaN(timeoutParam) || timeoutParam <= 0 ? 3000 : Math.min(timeoutParam, 30000);This also adds an upper bound to prevent excessively long timeouts.
98-105: Consider centralizing critical services configuration.The hardcoded list of critical service slugs could be moved to a configuration file or the service catalog metadata to make it more maintainable and allow services to self-declare their criticality.
This would allow the service catalog entries to include a
critical: booleanfield, making the system more flexible as new critical services are added.pmoves/Makefile (2)
43-50: Smoke targets & orchestration look good; verify pytest-xdist and doc alignmentThe new orchestration flow (
up-obs→up-all-new/up-core/up-minimal) and the pytest-based smoke targets (test-smoke*) give a much clearer entrypoint for bring-up and validation; the standardall/testtargets are also a nice DX win.A couple of things to double-check:
test-smoke-parallelusespytest -n auto, which requirespytest-xdistin the environment. Make sure that’s included in the relevant requirements so this target doesn’t fail unexpectedly.up-agentsandup-tensorzeroare defined earlier in the file as well; GNU Make will concatenate recipes, which works but is a bit surprising. At some point it may be worth consolidating these into a single definition per target so behavior is obvious.- Given these become canonical test/bring-up entrypoints, it’s probably time to scan
pmoves/docs/NEXT_STEPS.md,pmoves/docs/PMOVES.AI PLANS/ROADMAP.md, anddocs/LOCAL_CI_CHECKS.mdfor any oldermake smoke/up-allreferences and update them to mentionmake -C pmoves test/test-smoke/up-all-new. Based on learnings.Also applies to: 71-79, 188-201, 286-347, 360-373, 1939-1967
43-47: Confirm presence and permissions of new integration setup scriptsThe new
SCRIPTS := scriptsvar andsetup-*targets (setup-all-integrations,setup-agent-zero,setup-archon,setup-supaserch,setup-deepresearch,setup-flute-gateway,setup-extract-worker) are a nice way to standardize TAC 2 provisioning.Just make sure:
- The corresponding
scripts/setup-*.shfiles exist underpmoves/scripts/.- They’re executable (
chmod +x) and behave correctly when invoked with thestatussubcommand.- Any relevant runbooks (e.g., PMOVES.AI PLANS, integration docs) reference these new setup targets so future operators know where to start. Based on learnings.
Also applies to: 2957-2989
pmoves/ui/__tests__/services-pages.test.tsx (1)
6-7: Tests align with centralized Services UI; consider reducing duplication and run UI smokeThe updated test correctly targets the new “Services” heading and checks for a representative set of core services on the dashboard, which matches the centralized catalog-driven UI.
Two small suggestions:
- Since you already import
SERVICE_CATALOG, you could derive the sample list from it (e.g., by picking a few known slugs and reading their titles) to avoid tests drifting when titles change.- Per
pmoves/AGENTS.mdguidelines forpmoves/ui/**/*, make sure to run:make -C pmoves notebook-workbench-smoke ARGS="--thread=<some-uuid>"to lint the Next.js bundle and validate Supabase connectivity before merging. Based on learnings.
Also applies to: 35-49
pmoves/ui/components/services/CategorySection.tsx (1)
12-17: CategorySection wiring looks solid; clean up unusedcollapsedpropThe category header, health summary, and ServiceCard wiring all look consistent with
ServiceCategoryandServiceHealthMap, and the health counts are computed correctly.The only minor nit is that
collapsed?: booleanis defined onCategorySectionPropsbut never used in the component. If you don’t plan to support collapsible sections, consider removing it (or, if you do, wiring it into the layout) to keep the public API surface lean.Also applies to: 34-41, 56-63, 67-76
pmoves/ui/app/api/services/route.ts (2)
27-27: Unsafe type assertion forcategoryparameter.The
categoryis cast directly toServiceCategory | nullwithout validation. If a client sends an invalid category string,getServicesByCategorywill return an empty array (based on the|| []fallback in the helper), but the response would incorrectly suggest filtering occurred.Consider validating the category:
Proposed fix
+import { + SERVICE_CATALOG, + SERVICES_BY_CATEGORY, + getServiceBySlug, + getServicesByCategory, + type ServiceCategory, +} from '@/lib/serviceCatalog'; + +const VALID_CATEGORIES = Object.keys(SERVICES_BY_CATEGORY) as ServiceCategory[]; + export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; - const category = searchParams.get('category') as ServiceCategory | null; + const categoryParam = searchParams.get('category'); + const category = categoryParam && VALID_CATEGORIES.includes(categoryParam as ServiceCategory) + ? (categoryParam as ServiceCategory) + : null;
55-63: Inconsistent case handling in search filter.The
slugcomparison on line 61 doesn't applytoLowerCase(), unliketitleandsummary. This means slug matching is case-sensitive while the others are case-insensitive.Proposed fix
if (searchQuery) { const query = searchQuery.toLowerCase(); services = services.filter( (s) => s.title.toLowerCase().includes(query) || s.summary.toLowerCase().includes(query) || - s.slug.includes(query) + s.slug.toLowerCase().includes(query) ); }pmoves/ui/components/hub/SystemStatsBar.tsx (2)
39-40: UnusedstatusBgvariable.The
statusBgvariable is computed but never used in the component's JSX. Consider removing it or applying it where intended.Proposed fix (remove unused variable)
const statusColor = percentage >= 80 ? 'text-cata-forest' : percentage >= 50 ? 'text-cata-gold' : 'text-cata-ember'; - - const statusBg = percentage >= 80 ? 'bg-cata-forest/10' : - percentage >= 50 ? 'bg-cata-gold/10' : 'bg-cata-ember/10';
43-50: Consider handling edge case for future dates.If
dateis slightly in the future (clock skew),diffwould be negative, displaying something like "-1s ago". This is unlikely but could be guarded.Proposed fix
const formatTime = (date: Date | null) => { if (!date) return 'Never'; const now = new Date(); const diff = Math.floor((now.getTime() - date.getTime()) / 1000); + if (diff < 0) return 'Just now'; if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; return `${Math.floor(diff / 3600)}h ago`; };pmoves/ui/components/hub/SystemHubSection.tsx (1)
51-67: Consider extracting hub data fetch logic to reduce duplication.The same fetch logic for
/api/services-hubappears in bothuseEffectandhandleRefresh. Extracting it would improve maintainability.Proposed refactor
+ // Reusable fetch function + const fetchHubData = useCallback(async () => { + try { + const res = await fetch('/api/services-hub'); + if (res.ok) { + const data = await res.json(); + setHubData(data); + } + } catch (err) { + console.error('Failed to fetch hub data:', err); + } + }, []); + // Fetch hub data (catalog + tier stats) useEffect(() => { - async function fetchHubData() { - try { - const res = await fetch('/api/services-hub'); - if (res.ok) { - const data = await res.json(); - setHubData(data); - } - } catch (err) { - console.error('Failed to fetch hub data:', err); - } finally { - setIsLoading(false); - } - } - - fetchHubData(); - }, []); + fetchHubData().finally(() => setIsLoading(false)); + }, [fetchHubData]); // Handle manual refresh const handleRefresh = async () => { await refresh(); - // Also refetch hub data - try { - const res = await fetch('/api/services-hub'); - if (res.ok) { - const data = await res.json(); - setHubData(data); - } - } catch (err) { - console.error('Failed to refresh hub data:', err); - } + await fetchHubData(); };pmoves/ui/app/dashboard/services/page.tsx (1)
74-101: Consider combining iterations over SERVICE_CATALOG for efficiency.The component iterates SERVICE_CATALOG multiple times (filtering in
filteredServices, calculatingtierStats, and computingoverallStats). While this is fine for ~94 services, combining these into a single pass could be more efficient.Example optimization
const { filteredServices, tierStats, overallStats } = useMemo(() => { const tierStatsMap: Record<string, { total: number; healthy: number }> = {}; const filtered: typeof SERVICE_CATALOG = []; let healthy = 0, unhealthy = 0; for (const service of SERVICE_CATALOG) { // Build tier stats const tier = service.category; if (!tierStatsMap[tier]) tierStatsMap[tier] = { total: 0, healthy: 0 }; tierStatsMap[tier].total++; const serviceHealth = health[service.slug]; if (serviceHealth?.status === 'healthy') { tierStatsMap[tier].healthy++; healthy++; } else if (serviceHealth?.status === 'unhealthy') { unhealthy++; } // Filter const matchesTier = activeTier === 'all' || service.category === activeTier; const matchesSearch = /* ... */; if (matchesTier && matchesSearch) filtered.push(service); } // ... transform tierStatsMap to array, compute overall return { filteredServices: filtered, tierStats, overallStats }; }, [activeTier, searchQuery, health]);pmoves/ui/components/services/ServiceHealthIndicator.tsx (2)
33-38: Consider usinganimate-pulseinstead ofanimate-spinfor checking state.Since the indicator is a circle,
animate-spinrotation is not visually perceptible.animate-pulsewould provide clearer visual feedback that a check is in progress.Proposed fix
const statusClasses: Record<ServiceHealthStatus, string> = { healthy: 'bg-cata-forest shadow-[0_0_8px_rgba(0,255,136,0.5)]', unhealthy: 'bg-cata-ember shadow-[0_0_8px_rgba(255,61,61,0.5)]', unknown: 'bg-ink-muted', - checking: 'bg-cata-gold animate-spin', + checking: 'bg-cata-gold animate-pulse', };
92-97: Consider reusinggetStatusTextfrom serviceHealth.ts.The
statusTextmapping duplicates the logic ingetStatusTextfrom@/lib/serviceHealth. Importing and reusing it would ensure consistency and reduce duplication.Proposed fix
-import { type ServiceHealthStatus } from '@/lib/serviceHealth'; +import { type ServiceHealthStatus, getStatusText } from '@/lib/serviceHealth'; // Then in ServiceHealthBadge: - const statusText: Record<ServiceHealthStatus, string> = { - healthy: 'Online', - unhealthy: 'Offline', - unknown: 'Unknown', - checking: 'Checking...', - }; // And in the JSX: - {showText && <span>{statusText[status]}</span>} + {showText && <span>{getStatusText(status)}</span>}pmoves/ui/lib/serviceHealth.ts (1)
118-130: Consider single-pass statistics calculation.The current implementation iterates over the results array three times to calculate statistics. For 94+ services, this is negligible, but a single-pass reduction would be slightly more efficient.
🔎 Optional single-pass optimization
- // Calculate statistics - const healthy = results.filter((r) => r.status === 'healthy').length; - const unhealthy = results.filter((r) => r.status === 'unhealthy').length; - const unknown = results.filter((r) => r.status === 'unknown').length; + // Calculate statistics in single pass + const stats = results.reduce( + (acc, r) => { + acc[r.status] = (acc[r.status] || 0) + 1; + return acc; + }, + {} as Record<string, number> + ); + const healthy = stats.healthy || 0; + const unhealthy = stats.unhealthy || 0; + const unknown = stats.unknown || 0;pmoves/ui/lib/useServiceHealth.ts (1)
126-135: Minor: RedundantmountedRef.current = trueassignment.Line 127 sets
mountedRef.current = true, but it's already initialized totrueon line 46. This is harmless but redundant. The cleanup logic is correct.🔎 Suggested cleanup
useEffect(() => { - mountedRef.current = true; - return () => { mountedRef.current = false; if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []);
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
.claude/learnings/pr366-self-hosted-runner-2025-12.md.github/workflows/chit-contract.yml.github/workflows/python-tests.yml.github/workflows/webhook-smoke.yml.gitmodulespmoves/Makefilepmoves/ui/__tests__/services-pages.test.tsxpmoves/ui/app/api/health-all/route.tspmoves/ui/app/api/services-hub/route.tspmoves/ui/app/api/services/route.tspmoves/ui/app/dashboard/services/page.tsxpmoves/ui/app/page.tsxpmoves/ui/components/hub/SystemHubSection.tsxpmoves/ui/components/hub/SystemStatsBar.tsxpmoves/ui/components/services/CategorySection.tsxpmoves/ui/components/services/ServiceCard.tsxpmoves/ui/components/services/ServiceGrid.tsxpmoves/ui/components/services/ServiceHealthIndicator.tsxpmoves/ui/components/services/ServicesHeaderActions.tsxpmoves/ui/components/services/TierNavigation.tsxpmoves/ui/components/services/TierOverview.tsxpmoves/ui/components/services/index.tspmoves/ui/e2e/services.spec.tspmoves/ui/lib/serviceCatalog.tspmoves/ui/lib/serviceHealth.tspmoves/ui/lib/useServiceHealth.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/{.github,ci,lint,scripts}/**/*.{py,js,yaml,yml}
📄 CodeRabbit inference engine (GEMINI.md)
Draft a CI-oriented pack manifest linter for validation
Files:
.github/workflows/chit-contract.yml.github/workflows/python-tests.yml.github/workflows/webhook-smoke.yml
pmoves/ui/**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (pmoves/AGENTS.md)
UI updates: run
make -C pmoves notebook-workbench-smoke ARGS="--thread=<uuid>"to lint the Next.js bundle and validate Supabase connectivity; referencepmoves/docs/UI_NOTEBOOK_WORKBENCH.md
Files:
pmoves/ui/components/services/index.tspmoves/ui/components/services/CategorySection.tsxpmoves/ui/lib/serviceCatalog.tspmoves/ui/components/services/ServiceGrid.tsxpmoves/ui/app/api/health-all/route.tspmoves/ui/__tests__/services-pages.test.tsxpmoves/ui/app/api/services-hub/route.tspmoves/ui/components/services/ServiceCard.tsxpmoves/ui/components/services/ServicesHeaderActions.tsxpmoves/ui/components/hub/SystemStatsBar.tsxpmoves/ui/app/dashboard/services/page.tsxpmoves/ui/components/services/ServiceHealthIndicator.tsxpmoves/ui/components/hub/SystemHubSection.tsxpmoves/ui/app/api/services/route.tspmoves/ui/lib/serviceHealth.tspmoves/ui/components/services/TierNavigation.tsxpmoves/ui/app/page.tsxpmoves/ui/components/services/TierOverview.tsxpmoves/ui/lib/useServiceHealth.tspmoves/ui/e2e/services.spec.ts
.claude/**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
Mirror service/endpoint changes in .claude/context/services-catalog.md and add command stubs in .claude/commands/ when relevant
Files:
.claude/learnings/pr366-self-hosted-runner-2025-12.md
🧠 Learnings (19)
📓 Common learnings
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: PRs should include clear description, linked issues, affected services, run/rollback notes, and screenshots for UI/flows; start from STARTER_PR_BODY.md and adjust sections as needed
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Applies to pmoves/ui/**/*.{js,jsx,ts,tsx} : UI updates: run `make -C pmoves notebook-workbench-smoke ARGS="--thread=<uuid>"` to lint the Next.js bundle and validate Supabase connectivity; reference `pmoves/docs/UI_NOTEBOOK_WORKBENCH.md`
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: PRs should include: clear description, linked issues, affected services, run/rollback notes, and screenshots for UI/flows
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: UI updates: run make -C pmoves notebook-workbench-smoke ARGS='--thread=<uuid>' to lint the Next.js bundle and validate Supabase connectivity; reference pmoves/docs/UI_NOTEBOOK_WORKBENCH.md
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Applies to services/**/README.md : Update services/*/README.md and pmoves/docs/PMOVES.AI PLANS/ runbooks when touching service operational code
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Mandatory context before changes: read pmoves/docs/PMOVES.AI PLANS/ROADMAP.md and pmoves/docs/NEXT_STEPS.md to align with current sprint focus
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Applies to pmoves/services/{agent-zero,archon}/**/*.py : For Agents/Archon full-stack validation, follow the 'All Services Up, Then Tests' section in `pmoves/docs/SMOKETESTS.md` and use `make -C pmoves agents-headless-smoke`, `make -C pmoves smoke-gpu`, and `make -C pmoves verify-all`
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Agents/Archon: for full-stack validation, follow the 'All Services Up, Then Tests' section in pmoves/docs/SMOKETESTS.md and the Archon service guide
📚 Learning: 2025-12-07T11:03:53.415Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Applies to pmoves/docs/LOCAL_CI_CHECKS.md : Before pushing, mirror the GitHub Actions checks documented in `docs/LOCAL_CI_CHECKS.md` (pytest suites, `make chit-contract-check`, `make jellyfin-verify`, SQL policy lint, env preflight)
Applied to files:
.github/workflows/chit-contract.yml
📚 Learning: 2025-12-15T12:03:17.291Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Before pushing, mirror the GitHub Actions checks documented in docs/LOCAL_CI_CHECKS.md (pytest suites, make chit-contract-check, make jellyfin-verify when publisher is affected, SQL policy lint, env preflight)
Applied to files:
.github/workflows/chit-contract.yml.claude/learnings/pr366-self-hosted-runner-2025-12.md
📚 Learning: 2025-12-15T12:03:17.291Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Applies to services/**/README.md : Update services/*/README.md and pmoves/docs/PMOVES.AI PLANS/ runbooks when touching service operational code
Applied to files:
pmoves/ui/lib/serviceCatalog.tspmoves/ui/__tests__/services-pages.test.tsxpmoves/ui/lib/serviceHealth.tspmoves/ui/e2e/services.spec.tspmoves/Makefile
📚 Learning: 2025-12-15T12:03:17.291Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Applies to .claude/**/*.md : Mirror service/endpoint changes in .claude/context/services-catalog.md and add command stubs in .claude/commands/ when relevant
Applied to files:
pmoves/ui/lib/serviceCatalog.tspmoves/ui/e2e/services.spec.ts
📚 Learning: 2025-12-15T12:03:17.291Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Agents/Archon: for full-stack validation, follow the 'All Services Up, Then Tests' section in pmoves/docs/SMOKETESTS.md and the Archon service guide
Applied to files:
pmoves/ui/__tests__/services-pages.test.tsxpmoves/ui/e2e/services.spec.tspmoves/Makefile
📚 Learning: 2025-12-07T11:03:53.415Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Applies to pmoves/services/{agent-zero,archon}/**/*.py : For Agents/Archon full-stack validation, follow the 'All Services Up, Then Tests' section in `pmoves/docs/SMOKETESTS.md` and use `make -C pmoves agents-headless-smoke`, `make -C pmoves smoke-gpu`, and `make -C pmoves verify-all`
Applied to files:
pmoves/ui/__tests__/services-pages.test.tsxpmoves/ui/e2e/services.spec.tspmoves/Makefile
📚 Learning: 2025-12-07T11:03:53.415Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Applies to pmoves/ui/**/*.{js,jsx,ts,tsx} : UI updates: run `make -C pmoves notebook-workbench-smoke ARGS="--thread=<uuid>"` to lint the Next.js bundle and validate Supabase connectivity; reference `pmoves/docs/UI_NOTEBOOK_WORKBENCH.md`
Applied to files:
pmoves/ui/__tests__/services-pages.test.tsxpmoves/ui/app/page.tsxpmoves/Makefile
📚 Learning: 2025-12-15T12:02:50.226Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/docs/ARTSTUFF/realtime/.windsurfrules:0-0
Timestamp: 2025-12-15T12:02:50.226Z
Learning: Applies to pmoves/docs/ARTSTUFF/realtime/**/.gitignore : Project `.gitignore` must include entries for dynamically generated files, cloned repositories, downloaded files, virtual environments, and user-specific configurations
Applied to files:
.gitmodules
📚 Learning: 2025-12-15T12:03:17.291Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: PRs should include clear description, linked issues, affected services, run/rollback notes, and screenshots for UI/flows; start from STARTER_PR_BODY.md and adjust sections as needed
Applied to files:
.claude/learnings/pr366-self-hosted-runner-2025-12.md
📚 Learning: 2025-12-07T11:02:53.362Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T11:02:53.362Z
Learning: Call out mismatches between code changes and the runbooks (`pmoves/docs/NEXT_STEPS.md`, `pmoves/docs/ROADMAP.md`, `pmoves/docs/SESSION_IMPLEMENTATION_PLAN.md`) and suggest updates if missing
Applied to files:
.claude/learnings/pr366-self-hosted-runner-2025-12.mdpmoves/Makefile
📚 Learning: 2025-12-07T11:03:53.415Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Applies to pmoves/services/*/tests/test_*.py : Use `pytest` with `tests/` per service (e.g., `services/<name>/tests/test_*.py`) for testing
Applied to files:
pmoves/ui/e2e/services.spec.tspmoves/Makefile
📚 Learning: 2025-12-15T12:03:17.291Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Applies to services/**/tests/test_*.py : Use pytest for testing with tests/ per service (e.g., services/<name>/tests/test_*.py) and mock external systems (NATS, MinIO, Neo4j)
Applied to files:
pmoves/ui/e2e/services.spec.tspmoves/Makefile
📚 Learning: 2025-12-07T11:03:53.415Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Applies to pmoves/services/*/tests/test_*.py : Mock external systems (NATS, MinIO, Neo4j) and validate envelope/schema with sample payloads in tests
Applied to files:
pmoves/ui/e2e/services.spec.ts
📚 Learning: 2025-12-07T11:03:53.415Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Read `pmoves/docs/PMOVES.AI PLANS/ROADMAP.md` and `pmoves/docs/NEXT_STEPS.md` before making changes to align with current sprint focus
Applied to files:
pmoves/Makefile
📚 Learning: 2025-12-15T12:03:17.291Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: Mandatory context before changes: read pmoves/docs/PMOVES.AI PLANS/ROADMAP.md and pmoves/docs/NEXT_STEPS.md to align with current sprint focus
Applied to files:
pmoves/Makefile
📚 Learning: 2025-12-15T12:03:17.291Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-15T12:03:17.291Z
Learning: UI updates: run make -C pmoves notebook-workbench-smoke ARGS='--thread=<uuid>' to lint the Next.js bundle and validate Supabase connectivity; reference pmoves/docs/UI_NOTEBOOK_WORKBENCH.md
Applied to files:
pmoves/Makefile
📚 Learning: 2025-12-07T11:03:53.415Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Applies to pmoves/services/hi-rag-gateway/**/*.py : Hi-RAG gateway: after touching reranker or embedding code, run `make -C pmoves smoke-gpu` to validate FlagEmbedding/Qwen rerankers
Applied to files:
pmoves/Makefile
📚 Learning: 2025-12-07T11:03:53.415Z
Learnt from: CR
Repo: POWERFULMOVES/PMOVES.AI PR: 0
File: pmoves/AGENTS.md:0-0
Timestamp: 2025-12-07T11:03:53.415Z
Learning: Pin agent images by setting `AGENT_ZERO_IMAGE`, `ARCHON_IMAGE`, `ARCHON_UI_IMAGE`, and `PMOVES_YT_IMAGE` in `pmoves/env.shared`
Applied to files:
pmoves/Makefile
🧬 Code graph analysis (15)
pmoves/ui/components/services/CategorySection.tsx (3)
pmoves/ui/lib/serviceCatalog.ts (1)
ServiceCategory(7-18)pmoves/ui/lib/serviceHealth.ts (1)
ServiceHealthMap(20-25)pmoves/ui/components/services/ServiceCard.tsx (1)
ServiceCard(43-142)
pmoves/ui/components/services/ServiceGrid.tsx (3)
pmoves/ui/lib/serviceCatalog.ts (2)
ServiceDefinition(29-44)ServiceCategory(7-18)pmoves/ui/lib/serviceHealth.ts (1)
ServiceHealthMap(20-25)pmoves/ui/components/services/ServiceCard.tsx (1)
ServiceCard(43-142)
pmoves/ui/app/api/health-all/route.ts (1)
pmoves/ui/lib/serviceHealth.ts (4)
checkServiceHealth(136-141)getStatusText(196-207)checkAllServices(92-131)getHealthPercentage(146-149)
pmoves/ui/components/services/ServiceCard.tsx (2)
pmoves/ui/lib/serviceCatalog.ts (2)
ServiceColor(20-20)ServiceDefinition(29-44)pmoves/ui/lib/serviceHealth.ts (5)
ServiceHealthStatus(9-9)getStatusIndicatorClass(171-182)getStatusText(196-207)formatResponseTime(187-191)getStatusBadgeClass(154-166)
pmoves/ui/components/services/ServicesHeaderActions.tsx (2)
pmoves/ui/components/services/index.ts (1)
ServicesHeaderActions(4-4)pmoves/ui/lib/fluteClient.ts (1)
isHealthy(138-149)
pmoves/ui/components/hub/SystemStatsBar.tsx (1)
pmoves/ui/components/services/ServiceHealthIndicator.tsx (1)
ServiceHealthIndicator(21-61)
pmoves/ui/app/dashboard/services/page.tsx (3)
pmoves/ui/components/hub/SystemStatsBar.tsx (1)
SystemStatsBar(25-130)pmoves/ui/components/services/TierNavigation.tsx (1)
TierNavigation(90-163)pmoves/ui/components/services/ServiceHealthIndicator.tsx (1)
ServiceHealthIndicator(21-61)
pmoves/ui/components/services/ServiceHealthIndicator.tsx (1)
pmoves/ui/lib/serviceHealth.ts (1)
ServiceHealthStatus(9-9)
pmoves/ui/components/hub/SystemHubSection.tsx (4)
pmoves/ui/lib/serviceCatalog.ts (1)
ServiceCategory(7-18)pmoves/ui/components/services/TierOverview.tsx (2)
TierStats(13-19)TierOverviewGrid(301-324)pmoves/ui/lib/useServiceHealth.ts (1)
useServiceHealth(31-145)pmoves/ui/components/hub/SystemStatsBar.tsx (1)
SystemStatsBar(25-130)
pmoves/ui/app/api/services/route.ts (1)
pmoves/ui/lib/serviceCatalog.ts (5)
ServiceCategory(7-18)getServiceBySlug(810-812)SERVICE_CATALOG(65-786)getServicesByCategory(814-816)SERVICES_BY_CATEGORY(792-804)
pmoves/ui/lib/serviceHealth.ts (1)
pmoves/ui/lib/serviceCatalog.ts (2)
ServiceDefinition(29-44)SERVICE_CATALOG(65-786)
pmoves/ui/components/services/TierNavigation.tsx (1)
pmoves/ui/lib/serviceCatalog.ts (1)
ServiceCategory(7-18)
pmoves/ui/app/page.tsx (1)
pmoves/ui/components/hub/SystemHubSection.tsx (1)
SystemHubSection(40-184)
pmoves/ui/components/services/TierOverview.tsx (1)
pmoves/ui/lib/serviceCatalog.ts (1)
ServiceCategory(7-18)
pmoves/ui/lib/useServiceHealth.ts (1)
pmoves/ui/lib/serviceHealth.ts (2)
ServiceHealthMap(20-25)ServiceHealthStatus(9-9)
🪛 ast-grep (0.40.3)
pmoves/ui/e2e/services.spec.ts
[warning] 26-26: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(service.title, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🪛 GitHub Check: CodeQL
pmoves/ui/lib/serviceHealth.ts
[failure] 56-56: Resource exhaustion
This creates a timer with a user-controlled duration from a user-provided value.
This creates a timer with a user-controlled duration from a user-provided value.
🔇 Additional comments (27)
.github/workflows/webhook-smoke.yml (1)
17-18: LGTM: Security best practice applied.Adding explicit
contents: readpermission follows the principle of least privilege and aligns with GitHub's recommended security practices for Actions workflows..github/workflows/python-tests.yml (1)
26-27: LGTM: Consistent security hardening.The explicit permissions block is correctly applied, matching the pattern across all workflow files in this PR.
.github/workflows/chit-contract.yml (1)
23-24: LGTM: Security permissions properly configured.pmoves/ui/components/services/index.ts (1)
1-4: LGTM: Clean barrel export pattern.The barrel export follows standard TypeScript conventions and simplifies imports for consuming code.
pmoves/ui/e2e/services.spec.ts (1)
3-15: Good separation of catalog visibility vs. documentation tests.The split between
SERVICES(for visibility checks) andDOCUMENTED_SERVICES(for markdown rendering) is a clean architecture that acknowledges not all services have documentation pages yet. This prevents test failures as the catalog expands.pmoves/ui/app/api/services-hub/route.ts (2)
142-152: Good caching strategy for different modes.The differentiated cache headers for simple mode (60s) vs. full mode (10s) appropriately balance performance with data freshness, recognizing that health data is more volatile than catalog data.
Also applies to: 174-180
44-46: Category list matches ServiceCategory type definition.The hardcoded categories array exactly matches the
ServiceCategoryunion type fromserviceCatalog.ts. All 11 categories are present and accounted for..claude/learnings/pr366-self-hosted-runner-2025-12.md (1)
7-63: Learnings doc is clear and aligns with current toolingThis learnings note accurately captures the self-hosted runner migration work, Makefile/test updates, and validation commands. It’s a good reference for future PRs touching CI or Makefile targets and matches the current state of
pmoves/Makefileand workflows.Also applies to: 86-119, 129-163
pmoves/ui/app/api/services/route.ts (1)
73-81: Response structure is well-designed.The response includes services, grouped counts, total, and categories list—providing flexibility for consumers. The caching strategy with
s-maxage=10andstale-while-revalidate=30is appropriate for catalog data that changes infrequently.pmoves/ui/components/hub/SystemStatsBar.tsx (1)
52-129: Well-structured component with good responsive design.The component handles various states cleanly (checking, normal), uses proper disabled states, and has good responsive breakpoints for the stats breakdown.
pmoves/ui/app/page.tsx (1)
362-373: Clean integration of SystemHubSection.The page correctly delegates real-time health monitoring to the client component
SystemHubSection, which handles its own data fetching and polling. This is an appropriate architecture for live dashboard data.pmoves/ui/app/api/health-all/route.ts (2)
34-49: Single service check logic is correct.The condition properly detects a single slug and returns appropriate 404 for not found. The simple vs full response format provides good API flexibility.
75-81: Good response augmentation pattern.Spreading the result and adding the calculated
percentagekeeps the API response informative while leveraging the existingHealthCheckResultstructure.pmoves/ui/components/hub/SystemHubSection.tsx (1)
128-146: Good composition of health components.The
SystemStatsBarreceives computed stats and theTierOverviewGriduses the health map for per-service status. The tier expand toggle logic is clean.pmoves/ui/components/services/ServiceCard.tsx (2)
50-51: Hardcodedlocalhostin href construction.The URL is built with
http://localhost:${port}which only works in local development. If these services are accessed in other environments, this would need to be configurable.Is this dashboard intended only for local development use? If not, consider using an environment variable or relative paths:
const baseUrl = process.env.NEXT_PUBLIC_SERVICES_BASE_URL || 'http://localhost'; const href = primaryEndpoint ? `${baseUrl}:${primaryEndpoint.port}${primaryEndpoint.path}` : '#';
43-141: Well-structured card component with good null safety.The component handles optional props gracefully, provides sensible defaults, and the Tailwind JIT lookup pattern ensures class names are statically analyzable. Good use of conditional rendering for capabilities and health status.
pmoves/ui/app/dashboard/services/page.tsx (2)
170-172: Verify intended navigation behavior.The href here points to
/dashboard/services/${service.slug}(internal route), whileServiceCard.tsxuseshttp://localhost:${port}(direct service access). Ensure this difference is intentional—this page navigates to detail views whileServiceCardmay be for direct access.
237-247: Good empty state handling.The no-results UI provides clear feedback when filters yield no services, with actionable guidance to adjust criteria.
pmoves/ui/components/services/ServiceHealthIndicator.tsx (1)
48-61: Good accessibility implementation.The
aria-labelattribute provides screen reader support for the status indicator. The component is well-structured with flexible size and styling options.pmoves/ui/lib/serviceHealth.ts (5)
9-34: LGTM! Well-structured type definitions.The health status types and interfaces are cleanly organized and provide a good foundation for the health monitoring system. The
ServiceHealthMapwith index signature enables efficient O(1) lookups by slug.
133-141: LGTM!Clean implementation with proper handling for missing services.
143-149: LGTM!Properly handles the edge case of zero total services to avoid division by zero.
151-207: LGTM!The UI utility functions are well-implemented with proper default cases and consistent styling patterns. The animation classes provide good visual feedback for different health states.
209-221: LGTM!Clean conversion from array to indexed map structure for efficient lookups in UI components.
pmoves/ui/lib/useServiceHealth.ts (3)
6-24: LGTM!Well-defined types for the hook options and result. The status enum covers all necessary states for async operations.
31-46: LGTM!Good use of refs for tracking interval and mounted state, avoiding unnecessary re-renders.
147-180: LGTM!Clean helper functions with sensible defaults. Good use of TypeScript for type safety.
| [submodule "pmoves/vendor/e2b"] | ||
| path = pmoves/vendor/e2b |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify e2b submodule path migration is complete
echo "=== Checking for old path references ==="
rg "pmoves/pmoves/vendor/e2b" --type-add 'config:*.{yml,yaml,json,toml,ini,cfg}' --type config --type python --type typescript 2>&1 | head -20
echo ""
echo "=== Checking if new path exists ==="
if [ -d "pmoves/vendor/e2b" ]; then
echo "✅ New path exists: pmoves/vendor/e2b"
ls -la "pmoves/vendor/e2b" 2>/dev/null | head -5
else
echo "❌ New path not found: pmoves/vendor/e2b"
fi
echo ""
echo "=== Checking if old path exists (should not) ==="
if [ -d "pmoves/pmoves/vendor/e2b" ]; then
echo "⚠️ Old path still exists: pmoves/pmoves/vendor/e2b"
else
echo "✅ Old path removed: pmoves/pmoves/vendor/e2b"
fi
echo ""
echo "=== Verifying .git/config submodule entry ==="
if [ -f ".git/config" ]; then
grep -A 2 "submodule.*e2b" .git/config || echo "No submodule config found"
fi
echo ""
echo "=== Checking .gitmodules for submodule config ==="
if [ -f ".gitmodules" ]; then
grep -A 2 'submodule "pmoves/vendor/e2b"' .gitmodules
fi
echo ""
echo "=== Searching for any references to vendor/e2b in codebase ==="
rg "vendor/e2b" -t python -t typescript -t json 2>&1 | head -20Repository: POWERFULMOVES/PMOVES.AI
Length of output: 153
🏁 Script executed:
#!/bin/bash
# Simplified verification - avoid broad searches
echo "=== Directory checks ==="
echo "New path exists:"
[ -d "pmoves/vendor/e2b" ] && echo "✅ YES" || echo "❌ NO"
echo "Old path exists:"
[ -d "pmoves/pmoves/vendor/e2b" ] && echo "⚠️ YES (should not exist)" || echo "✅ NO"
echo ""
echo "=== .gitmodules content around e2b ==="
if [ -f ".gitmodules" ]; then
grep -A 2 "e2b" .gitmodules
fi
echo ""
echo "=== Search for old path in key config files ==="
rg -l "pmoves/pmoves/vendor/e2b" . --max-count=5 2>&1 | head -10
echo ""
echo "=== Check .git/config directly ==="
if [ -f ".git/config" ]; then
grep -A 3 "e2b" .git/config || echo "(no e2b entry found)"
fiRepository: POWERFULMOVES/PMOVES.AI
Length of output: 455
Complete the submodule path migration by moving the directory and reinitializing the submodule.
The .gitmodules configuration has been updated to reference pmoves/vendor/e2b, but the physical directory has not been moved. The directory still exists at the old path pmoves/pmoves/vendor/e2b, and the new path does not exist. This creates a broken submodule state.
Required actions:
- Move the directory from
pmoves/pmoves/vendor/e2btopmoves/vendor/e2b - Reinitialize the submodule to register it with git:
git submodule update --init --recursive - Verify the .git/config contains the correct submodule entry after reinitialization
🤖 Prompt for AI Agents
In .gitmodules around lines 99-100 the submodule path was changed to
pmoves/vendor/e2b but the repository still has the submodule checked out at
pmoves/pmoves/vendor/e2b, causing a broken submodule; move the directory from
pmoves/pmoves/vendor/e2b to pmoves/vendor/e2b, then run git submodule update
--init --recursive to reinitialize and register the submodule, and finally open
.git/config to confirm the submodule entry points to pmoves/vendor/e2b (adjust
.git/config manually if needed).
| */ | ||
| export async function GET(request: NextRequest) { | ||
| const searchParams = request.nextUrl.searchParams; | ||
| const timeout = parseInt(searchParams.get('timeout') || '5000', 10); |
There was a problem hiding this comment.
Validate timeout parameter to prevent NaN.
If the timeout query param contains a non-numeric string, parseInt returns NaN, which could cause unexpected behavior in checkAllServices.
Proposed fix
- const timeout = parseInt(searchParams.get('timeout') || '5000', 10);
+ const timeoutParam = parseInt(searchParams.get('timeout') || '5000', 10);
+ const timeout = Number.isNaN(timeoutParam) ? 5000 : Math.min(Math.max(timeoutParam, 1000), 30000);This also adds bounds to prevent excessively short or long timeouts.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const timeout = parseInt(searchParams.get('timeout') || '5000', 10); | |
| const timeoutParam = parseInt(searchParams.get('timeout') || '5000', 10); | |
| const timeout = Number.isNaN(timeoutParam) ? 5000 : Math.min(Math.max(timeoutParam, 1000), 30000); |
🤖 Prompt for AI Agents
In pmoves/ui/app/api/health-all/route.ts around line 22, the timeout parsing
uses parseInt directly which can produce NaN for non-numeric input; validate and
sanitize the value by attempting to parse (e.g. Number or parseInt), detect NaN
and fall back to the default (5000), then clamp the numeric timeout to
reasonable bounds (e.g. min 1000, max 30000) before passing it into
checkAllServices so extremely small or large values are prevented.
| 'agents', 'gpu', 'media', 'llm', 'ui', 'integration' | ||
| ]; | ||
|
|
||
| const tierStats: Record<string, TierStats> = {}; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Fix type annotation for tierStats.
The tierStats object is typed as Record<string, TierStats> but should be Record<ServiceCategory, TierStats> to match the return type and provide better type safety.
🔎 Proposed fix
- const tierStats: Record<string, TierStats> = {};
+ const tierStats: Partial<Record<ServiceCategory, TierStats>> = {};Note: Using Partial<> is appropriate here since the object is built incrementally in the loop.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const tierStats: Record<string, TierStats> = {}; | |
| const tierStats: Partial<Record<ServiceCategory, TierStats>> = {}; |
🤖 Prompt for AI Agents
In pmoves/ui/app/api/services-hub/route.ts around line 49, the tierStats
variable is currently typed as Record<string, TierStats>; change it to
Partial<Record<ServiceCategory, TierStats>> when declaring (e.g. const
tierStats: Partial<Record<ServiceCategory, TierStats>> = {}), populate it as you
do in the loop, and before returning ensure you either cast it to
Record<ServiceCategory, TierStats> or build a complete Record by filling missing
ServiceCategory keys so the function matches its declared return type.
| if (isLoading || !hubData) { | ||
| return ( | ||
| <section className="relative py-32 px-6 lg:px-12 bg-void-elevated"> | ||
| <div className="max-w-7xl mx-auto text-center"> | ||
| <div className="inline-flex items-center gap-3 text-cata-cyan"> | ||
| <div className="w-4 h-4 border-2 border-cata-cyan border-t-transparent rounded-full animate-spin" /> | ||
| <span className="font-pixel text-xs uppercase">Loading Service Hub...</span> | ||
| </div> | ||
| </div> | ||
| </section> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Missing error state UI for failed fetch.
If the fetch to /api/services-hub fails, isLoading becomes false but hubData remains null, causing the loading spinner to show indefinitely. Consider adding an error state.
Proposed enhancement
const [hubData, setHubData] = useState<SystemHubData | null>(null);
const [expandedTier, setExpandedTier] = useState<ServiceCategory | null>(null);
const [isLoading, setIsLoading] = useState(true);
+ const [fetchError, setFetchError] = useState<string | null>(null);
// In fetch logic:
} catch (err) {
console.error('Failed to fetch hub data:', err);
+ setFetchError('Failed to load service hub data');
}
- if (isLoading || !hubData) {
+ if (isLoading) {
return (/* loading spinner */);
}
+
+ if (fetchError || !hubData) {
+ return (
+ <section className="relative py-32 px-6 lg:px-12 bg-void-elevated">
+ <div className="max-w-7xl mx-auto text-center">
+ <div className="text-cata-ember font-display">
+ {fetchError || 'Failed to load service hub'}
+ </div>
+ <button onClick={handleRefresh} className="btn-ghost mt-4">
+ Retry
+ </button>
+ </div>
+ </section>
+ );
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In pmoves/ui/components/hub/SystemHubSection.tsx around lines 84 to 95, the
component only renders a loading spinner when isLoading or hubData is falsy, so
a failed fetch leaves hubData null and the UI stuck; update the render logic to
handle a failed fetch by adding an explicit error state branch (e.g., when
!isLoading && !hubData) that shows a user-friendly error message and a retry
button (or link) which re-triggers the fetch, and optionally log/display the
error details; ensure accessibility (aria-live) and consistent styling with the
existing section layout.
| return services.filter((service) => { | ||
| // Category filter | ||
| if (selectedCategory !== 'all' && service.category !== selectedCategory) { | ||
| return false; | ||
| } | ||
|
|
||
| // Search filter | ||
| if (searchQuery) { | ||
| const query = searchQuery.toLowerCase(); | ||
| return ( | ||
| service.title.toLowerCase().includes(query) || | ||
| service.summary.toLowerCase().includes(query) || | ||
| service.slug.includes(query) || | ||
| service.capabilities?.some((cap) => cap.toLowerCase().includes(query)) | ||
| ); | ||
| } | ||
|
|
||
| return true; | ||
| }); | ||
| }, [services, selectedCategory, searchQuery]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the file
find . -name "ServiceGrid.tsx" -type fRepository: POWERFULMOVES/PMOVES.AI
Length of output: 114
🏁 Script executed:
# Then read the file to verify the exact code at lines 31-50
if [ -f "pmoves/ui/components/services/ServiceGrid.tsx" ]; then
cat -n "pmoves/ui/components/services/ServiceGrid.tsx" | sed -n '20,60p'
fiRepository: POWERFULMOVES/PMOVES.AI
Length of output: 1710
🏁 Script executed:
# Check if this file exists and get its total line count
if [ -f "pmoves/ui/components/services/ServiceGrid.tsx" ]; then
wc -l "pmoves/ui/components/services/ServiceGrid.tsx"
else
echo "File not found at expected path"
fiRepository: POWERFULMOVES/PMOVES.AI
Length of output: 116
🏁 Script executed:
# Search for ServiceGrid in the codebase to confirm location
fd "ServiceGrid" --type fRepository: POWERFULMOVES/PMOVES.AI
Length of output: 112
🏁 Script executed:
# Search for Service type definition
rg "interface Service|type Service" --type ts --type tsx -A 5Repository: POWERFULMOVES/PMOVES.AI
Length of output: 94
🏁 Script executed:
# Look for slug patterns in service data or fixtures
rg "slug.*:" --type ts --type tsx -B 2 -A 1 | head -50Repository: POWERFULMOVES/PMOVES.AI
Length of output: 94
🏁 Script executed:
# Check if there are any services defined with mixed-case slugs
rg "slug\s*:" pmoves/ui -A 1 -B 1Repository: POWERFULMOVES/PMOVES.AI
Length of output: 13175
Normalize slug comparison in search to match other fields
Search normalizes the query to lowercase and lowercases title, summary, and each capability, but slug is compared as:
service.slug.includes(query)This makes slug matching case-sensitive while the rest is case-insensitive. To keep behavior consistent, normalize the slug comparison too:
- service.title.toLowerCase().includes(query) ||
- service.summary.toLowerCase().includes(query) ||
- service.slug.includes(query) ||
+ service.title.toLowerCase().includes(query) ||
+ service.summary.toLowerCase().includes(query) ||
+ service.slug.toLowerCase().includes(query) ||
service.capabilities?.some((cap) => cap.toLowerCase().includes(query))🤖 Prompt for AI Agents
In pmoves/ui/components/services/ServiceGrid.tsx around lines 31 to 50, the slug
comparison in the search filter is currently case-sensitive while other fields
are lowercased; update the comparison to normalize the slug too (e.g., use
service.slug?.toLowerCase().includes(query) or
service.slug.toLowerCase().includes(query) if slug is always defined) so slug
matching is case-insensitive and consistent with title/summary/capabilities.
| export function TierOverviewCard({ | ||
| tier, | ||
| stats, | ||
| healthMap, | ||
| isExpanded = false, | ||
| onExpandChange, | ||
| className = '', | ||
| }: TierOverviewProps) { | ||
| const info = TIER_INFO[tier]; | ||
| const colors = COLOR_CLASSES[info.color]; | ||
| const [localExpanded, setLocalExpanded] = useState(isExpanded); | ||
|
|
||
| const expanded = onExpandChange !== undefined ? isExpanded : localExpanded; | ||
| const setExpanded = (value: boolean) => { | ||
| if (onExpandChange) { | ||
| onExpandChange(tier, value); | ||
| } else { | ||
| setLocalExpanded(value); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "TierOverview.tsx" -type fRepository: POWERFULMOVES/PMOVES.AI
Length of output: 115
🏁 Script executed:
cat -n pmoves/ui/components/services/TierOverview.tsx | head -200Repository: POWERFULMOVES/PMOVES.AI
Length of output: 6373
🏁 Script executed:
cat -n pmoves/ui/components/services/TierOverview.tsx | tail -n +170 | head -150Repository: POWERFULMOVES/PMOVES.AI
Length of output: 5839
🏁 Script executed:
cat -n pmoves/ui/components/services/TierOverview.tsx | tail -n +315 | head -20Repository: POWERFULMOVES/PMOVES.AI
Length of output: 370
Fix controlled expand/collapse wiring in TierOverviewGrid
With the current wiring, controlled cards can't actually collapse: when a card toggles from expanded → collapsed, onExpandChange is called with expanded = false, but TierOverviewGrid silently ignores that case:
onExpandChange={onTierExpand ? (t, exp) => {
if (exp) onTierExpand(t);
} : undefined}Since the parent's expandedTier never changes on collapse, isExpanded stays true and the UI doesn't respond to clicks.
Delegate toggle logic fully to the parent by ignoring the exp flag and always calling onTierExpand(t):
- isExpanded={expandedTier === tier}
- onExpandChange={onTierExpand ? (t, exp) => {
- if (exp) onTierExpand(t);
- } : undefined}
+ isExpanded={expandedTier === tier}
+ onExpandChange={onTierExpand ? (t) => {
+ onTierExpand(t);
+ } : undefined}The parent (e.g., SystemHubSection) can then implement "click again to collapse" by toggling expandedTier between tier and null.
Additionally, status is computed in TierOverviewCard (line 173) but never used, and healthMap is threaded through as a prop but also unused—consider removing or using them.
🤖 Prompt for AI Agents
In pmoves/ui/components/services/TierOverview.tsx around lines 151 to 170, the
controlled expand/collapse handler passed from TierOverviewGrid is ignoring
collapse events; update the wiring so the parent fully owns toggle logic: when
mapping onTierExpand into the child, stop conditionalizing on the exp flag and
always call onTierExpand(tier) (i.e., onExpandChange={onTierExpand ? (t) =>
onTierExpand(t) : undefined}) so the parent can decide to expand or collapse,
and remove or use the unused local `status` and `healthMap` props (delete them
if not needed).
| slug: 'presign', | ||
| title: 'Presign', | ||
| summary: 'MinIO URL presigner for secure downloads', | ||
| category: 'workers', | ||
| color: 'ember', | ||
| endpoints: [ | ||
| { name: 'API', port: '8088', path: '/', type: 'api' }, | ||
| { name: 'Health', port: '8080', path: '/healthz', type: 'health' }, | ||
| ], | ||
| capabilities: ['URL presigning', 'Secure downloads'], | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's confirm the presign service definition in serviceCatalog.ts
cd pmoves && head -450 ui/lib/serviceCatalog.ts | tail -50Repository: POWERFULMOVES/PMOVES.AI
Length of output: 190
🏁 Script executed:
# Search the Makefile for presign health check references
cd pmoves && grep -n "presign" MakefileRepository: POWERFULMOVES/PMOVES.AI
Length of output: 1800
🏁 Script executed:
# Check if .claude/context/services-catalog.md exists
find . -path "./.claude/context/services-catalog.md" -type f 2>/dev/nullRepository: POWERFULMOVES/PMOVES.AI
Length of output: 104
🏁 Script executed:
# Also search for any health check patterns in the Makefile
cd pmoves && grep -A5 -B5 "8088\|8080" Makefile | head -60Repository: POWERFULMOVES/PMOVES.AI
Length of output: 4110
🏁 Script executed:
# Read the serviceCatalog.ts file around the presign service definition (lines 430-445)
sed -n '430,445p' pmoves/ui/lib/serviceCatalog.tsRepository: POWERFULMOVES/PMOVES.AI
Length of output: 503
🏁 Script executed:
# Also check a wider range to confirm the exact structure
sed -n '420,450p' pmoves/ui/lib/serviceCatalog.tsRepository: POWERFULMOVES/PMOVES.AI
Length of output: 935
🏁 Script executed:
# Check .claude/context/services-catalog.md for presign entry
cat .claude/context/services-catalog.md | grep -A 10 "presign\|Presign"Repository: POWERFULMOVES/PMOVES.AI
Length of output: 962
Fix presign health endpoint port: should be 8088, not 8080
The presign service defines its health endpoint on port 8080, but the actual service runs on 8088. The Makefile smoke tests (http://localhost:8088/healthz) and brand-verify target confirm presign health is on 8088, and .claude/context/services-catalog.md already correctly documents this port. Changing the health endpoint port to 8080 will cause health monitoring and UIs to probe the wrong port.
Update the health endpoint to port 8088:
{
slug: 'presign',
title: 'Presign',
summary: 'MinIO URL presigner for secure downloads',
category: 'workers',
color: 'ember',
endpoints: [
{ name: 'API', port: '8088', path: '/', type: 'api' },
- { name: 'Health', port: '8080', path: '/healthz', type: 'health' },
+ { name: 'Health', port: '8088', path: '/healthz', type: 'health' },
],
capabilities: ['URL presigning', 'Secure downloads'],
},This will align serviceCatalog.ts with the actual service configuration and with .claude/context/services-catalog.md.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| slug: 'presign', | |
| title: 'Presign', | |
| summary: 'MinIO URL presigner for secure downloads', | |
| category: 'workers', | |
| color: 'ember', | |
| endpoints: [ | |
| { name: 'API', port: '8088', path: '/', type: 'api' }, | |
| { name: 'Health', port: '8080', path: '/healthz', type: 'health' }, | |
| ], | |
| capabilities: ['URL presigning', 'Secure downloads'], | |
| }, | |
| slug: 'presign', | |
| title: 'Presign', | |
| summary: 'MinIO URL presigner for secure downloads', | |
| category: 'workers', | |
| color: 'ember', | |
| endpoints: [ | |
| { name: 'API', port: '8088', path: '/', type: 'api' }, | |
| { name: 'Health', port: '8088', path: '/healthz', type: 'health' }, | |
| ], | |
| capabilities: ['URL presigning', 'Secure downloads'], | |
| }, |
🤖 Prompt for AI Agents
In pmoves/ui/lib/serviceCatalog.ts around lines 432 to 442, the presign
service's Health endpoint is incorrectly set to port 8080; change the Health
endpoint port to 8088 so it matches the actual service and docs. Update the
endpoints array entry { name: 'Health', port: '8080', path: '/healthz', type:
'health' } to use port '8088', then run the UI and smoke tests to verify
/healthz is reachable on 8088.
| export async function probeService( | ||
| service: ServiceDefinition, | ||
| timeout = 5000 | ||
| ): Promise<ServiceHealth> { | ||
| const startTime = performance.now(); | ||
|
|
||
| // If service has no health check, mark as unknown | ||
| if (!service.healthCheck) { | ||
| return { | ||
| slug: service.slug, | ||
| status: 'unknown', | ||
| lastCheck: new Date(), | ||
| }; | ||
| } | ||
|
|
||
| try { | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), timeout); |
There was a problem hiding this comment.
Add bounds validation for user-controlled timeout to address resource exhaustion concern.
The CodeQL static analysis flagged that the timeout parameter is user-controlled and passed directly to setTimeout. While the default is reasonable, consider adding bounds validation to prevent potential abuse or misconfiguration.
🔎 Proposed fix
export async function probeService(
service: ServiceDefinition,
timeout = 5000
): Promise<ServiceHealth> {
+ // Clamp timeout to reasonable bounds (100ms - 30s)
+ const safeTimeout = Math.max(100, Math.min(timeout, 30000));
const startTime = performance.now();
// If service has no health check, mark as unknown
if (!service.healthCheck) {
return {
slug: service.slug,
status: 'unknown',
lastCheck: new Date(),
};
}
try {
const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), timeout);
+ const timeoutId = setTimeout(() => controller.abort(), safeTimeout);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function probeService( | |
| service: ServiceDefinition, | |
| timeout = 5000 | |
| ): Promise<ServiceHealth> { | |
| const startTime = performance.now(); | |
| // If service has no health check, mark as unknown | |
| if (!service.healthCheck) { | |
| return { | |
| slug: service.slug, | |
| status: 'unknown', | |
| lastCheck: new Date(), | |
| }; | |
| } | |
| try { | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), timeout); | |
| export async function probeService( | |
| service: ServiceDefinition, | |
| timeout = 5000 | |
| ): Promise<ServiceHealth> { | |
| // Clamp timeout to reasonable bounds (100ms - 30s) | |
| const safeTimeout = Math.max(100, Math.min(timeout, 30000)); | |
| const startTime = performance.now(); | |
| // If service has no health check, mark as unknown | |
| if (!service.healthCheck) { | |
| return { | |
| slug: service.slug, | |
| status: 'unknown', | |
| lastCheck: new Date(), | |
| }; | |
| } | |
| try { | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), safeTimeout); |
🧰 Tools
🪛 GitHub Check: CodeQL
[failure] 56-56: Resource exhaustion
This creates a timer with a user-controlled duration from a user-provided value.
This creates a timer with a user-controlled duration from a user-provided value.
🤖 Prompt for AI Agents
In pmoves/ui/lib/serviceHealth.ts around lines 39 to 56, the function accepts a
user-controlled timeout and passes it directly to setTimeout; add bounds
validation to prevent resource exhaustion by sanitizing the value before use:
coerce the incoming timeout to a finite integer, then clamp it to a safe range
(for example, minTimeout = 100 ms and maxTimeout = 30000 ms or use
project-configured limits); if the value is missing or invalid fall back to the
existing default (5000), and use the sanitizedTimeout variable when creating the
AbortController timeout and any other timeout logic; optionally throw a clear
error or log and use the default when the value is out of bounds.
| // Extract health from hub response | ||
| if (data.health?.services) { | ||
| const healthMap: ServiceHealthMap = {}; | ||
| for (const service of data.health.services) { | ||
| healthMap[service.slug] = { | ||
| status: service.status, | ||
| responseTime: service.responseTime, | ||
| }; | ||
| } | ||
| setHealth(healthMap); | ||
| setLastUpdate(new Date()); | ||
| setStatus('success'); | ||
| } |
There was a problem hiding this comment.
Handle missing health data in response to avoid stuck loading state.
If the API response is successful but data.health?.services is undefined or falsy, the status remains 'loading' indefinitely since neither setStatus('success') nor setStatus('error') is called.
🔎 Proposed fix
// Extract health from hub response
if (data.health?.services) {
const healthMap: ServiceHealthMap = {};
for (const service of data.health.services) {
healthMap[service.slug] = {
status: service.status,
responseTime: service.responseTime,
};
}
setHealth(healthMap);
setLastUpdate(new Date());
setStatus('success');
+ } else {
+ // No health data in response, but request succeeded
+ setStatus('success');
+ setLastUpdate(new Date());
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Extract health from hub response | |
| if (data.health?.services) { | |
| const healthMap: ServiceHealthMap = {}; | |
| for (const service of data.health.services) { | |
| healthMap[service.slug] = { | |
| status: service.status, | |
| responseTime: service.responseTime, | |
| }; | |
| } | |
| setHealth(healthMap); | |
| setLastUpdate(new Date()); | |
| setStatus('success'); | |
| } | |
| // Extract health from hub response | |
| if (data.health?.services) { | |
| const healthMap: ServiceHealthMap = {}; | |
| for (const service of data.health.services) { | |
| healthMap[service.slug] = { | |
| status: service.status, | |
| responseTime: service.responseTime, | |
| }; | |
| } | |
| setHealth(healthMap); | |
| setLastUpdate(new Date()); | |
| setStatus('success'); | |
| } else { | |
| // No health data in response, but request succeeded | |
| setStatus('success'); | |
| setLastUpdate(new Date()); | |
| } |
🤖 Prompt for AI Agents
In pmoves/ui/lib/useServiceHealth.ts around lines 72 to 84, the code only
updates state when data.health.services is present which leaves status as
'loading' if services is missing; update the branch to handle the falsy case by
explicitly setting a fallback state (e.g., setHealth({}) or a minimal
ServiceHealthMap), setLastUpdate(new Date()), and setStatus('error') or another
appropriate terminal status so the hook doesn't remain stuck in 'loading';
implement this by adding an else path after the existing if that performs those
state updates.
| useEffect(() => { | ||
| if (!enabled) return; | ||
|
|
||
| setIsPolling(true); | ||
|
|
||
| // Initial fetch | ||
| fetchHealth(); | ||
|
|
||
| // Set up interval | ||
| intervalRef.current = setInterval(() => { | ||
| fetchHealth(); | ||
| }, pollInterval); | ||
|
|
||
| return () => { | ||
| if (intervalRef.current) { | ||
| clearInterval(intervalRef.current); | ||
| } | ||
| }; | ||
| }, [enabled, pollInterval, fetchHealth]); |
There was a problem hiding this comment.
isPolling state is not reset when polling is disabled.
When enabled becomes false, the effect returns early without updating isPolling, leaving it potentially stuck at true even though polling has stopped.
🔎 Proposed fix
useEffect(() => {
- if (!enabled) return;
+ if (!enabled) {
+ setIsPolling(false);
+ return;
+ }
setIsPolling(true);
// Initial fetch
fetchHealth();
// Set up interval
intervalRef.current = setInterval(() => {
fetchHealth();
}, pollInterval);
return () => {
+ setIsPolling(false);
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [enabled, pollInterval, fetchHealth]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (!enabled) return; | |
| setIsPolling(true); | |
| // Initial fetch | |
| fetchHealth(); | |
| // Set up interval | |
| intervalRef.current = setInterval(() => { | |
| fetchHealth(); | |
| }, pollInterval); | |
| return () => { | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); | |
| } | |
| }; | |
| }, [enabled, pollInterval, fetchHealth]); | |
| useEffect(() => { | |
| if (!enabled) { | |
| setIsPolling(false); | |
| return; | |
| } | |
| setIsPolling(true); | |
| // Initial fetch | |
| fetchHealth(); | |
| // Set up interval | |
| intervalRef.current = setInterval(() => { | |
| fetchHealth(); | |
| }, pollInterval); | |
| return () => { | |
| setIsPolling(false); | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); | |
| } | |
| }; | |
| }, [enabled, pollInterval, fetchHealth]); |
🤖 Prompt for AI Agents
In pmoves/ui/lib/useServiceHealth.ts around lines 103 to 121, the effect returns
early when enabled is false but does not reset isPolling or clear any existing
interval, so isPolling can remain true after polling stops; update the effect to
explicitly setIsPolling(false) and clearInterval(intervalRef.current) when
enabled is false (or in the early-return branch), and ensure the cleanup always
clears and nulls intervalRef.current and sets isPolling false on unmount so the
state correctly reflects that polling has stopped.
…es-ui # Conflicts: # .claude/learnings/pr366-self-hosted-runner-2025-12.md # pmoves/Makefile
* feat(ui): Centralized PMOVES UI with real-time health monitoring Implements TAC 1: Single branded dashboard showing all 94+ services with real-time health monitoring, eliminating URL jumping between services. Features: - Service catalog with 94+ services across 11 categories - Real-time health monitoring (30s auto-refresh) - Tier/category filtering on services dashboard - Expandable tier overview cards on landing page - System-wide health statistics bar - Search functionality for services New Components: - SystemStatsBar - Overall health percentage and service counts - TierNavigation - Category filter pills - TierOverview - Expandable tier summary cards - ServiceHealthIndicator - Visual health status dot - SystemHubSection - Landing page service hub New APIs: - GET /api/services - Service catalog with filtering - GET /api/health-all - Health check for all/specific services - GET /api/services-hub - Aggregated catalog + health endpoint New Hooks: - useServiceHealth - Client-side health polling with auto-refresh 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: resolve TypeScript build errors from PR review Fixes 4 critical type errors identified by PR review: - Change isChecking → isPolling (useServiceHealth returns isPolling) - Fix ServiceCategory import from serviceCatalog instead of serviceHealth - Add 'use client' directive to services/page.tsx for hooks - Remove metadata export (incompatible with client components) - Fix ServiceEndpoint.url → endpoints.length check Resolves: - Type error: Property 'isChecking' does not exist - Type error: Module has no exported member 'ServiceCategory' - Type error: React hooks require client component 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ci): address PR #366 CodeRabbit review feedback Add missing permissions blocks to 3 workflows and fix out-of-diff issues: - Add permissions: contents: read to chit-contract.yml, python-tests.yml, webhook-smoke.yml - Fix .gitmodules e2b submodule path (pmoves/pmoves/vendor/e2b → pmoves/vendor/e2b) - Add SCRIPTS variable to Makefile (was undefined) - Fix Makefile test-smoke targets (remove cd pmoves && for make -C compatibility) - Add standard Makefile targets (all, test, clean, .DEFAULT_GOAL) - Add learnings document 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(tests): update tests for TAC 1 centralized UI Updates Jest and E2E tests to work with the new SERVICE_CATALOG: - Changed heading check from "integration services" to "services" - Updated service list to use SERVICE_CATALOG entries - Separated catalog visibility tests from markdown doc tests Test results: - Jest: 10/11 passing (2 suites) - E2E: 4/4 passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Codex Agent <codex-agent@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(ui): Centralized PMOVES UI with real-time health monitoring Implements TAC 1: Single branded dashboard showing all 94+ services with real-time health monitoring, eliminating URL jumping between services. Features: - Service catalog with 94+ services across 11 categories - Real-time health monitoring (30s auto-refresh) - Tier/category filtering on services dashboard - Expandable tier overview cards on landing page - System-wide health statistics bar - Search functionality for services New Components: - SystemStatsBar - Overall health percentage and service counts - TierNavigation - Category filter pills - TierOverview - Expandable tier summary cards - ServiceHealthIndicator - Visual health status dot - SystemHubSection - Landing page service hub New APIs: - GET /api/services - Service catalog with filtering - GET /api/health-all - Health check for all/specific services - GET /api/services-hub - Aggregated catalog + health endpoint New Hooks: - useServiceHealth - Client-side health polling with auto-refresh 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: resolve TypeScript build errors from PR review Fixes 4 critical type errors identified by PR review: - Change isChecking → isPolling (useServiceHealth returns isPolling) - Fix ServiceCategory import from serviceCatalog instead of serviceHealth - Add 'use client' directive to services/page.tsx for hooks - Remove metadata export (incompatible with client components) - Fix ServiceEndpoint.url → endpoints.length check Resolves: - Type error: Property 'isChecking' does not exist - Type error: Module has no exported member 'ServiceCategory' - Type error: React hooks require client component 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ci): address PR #366 CodeRabbit review feedback Add missing permissions blocks to 3 workflows and fix out-of-diff issues: - Add permissions: contents: read to chit-contract.yml, python-tests.yml, webhook-smoke.yml - Fix .gitmodules e2b submodule path (pmoves/pmoves/vendor/e2b → pmoves/vendor/e2b) - Add SCRIPTS variable to Makefile (was undefined) - Fix Makefile test-smoke targets (remove cd pmoves && for make -C compatibility) - Add standard Makefile targets (all, test, clean, .DEFAULT_GOAL) - Add learnings document 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(tests): update tests for TAC 1 centralized UI Updates Jest and E2E tests to work with the new SERVICE_CATALOG: - Changed heading check from "integration services" to "services" - Updated service list to use SERVICE_CATALOG entries - Separated catalog visibility tests from markdown doc tests Test results: - Jest: 10/11 passing (2 suites) - E2E: 4/4 passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Codex Agent <codex-agent@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Centralized PMOVES UI with:
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Chores
✏️ Tip: You can customize this high-level summary in your review settings.