Skip to content

feat(ui): Centralized PMOVES UI with real-time health monitoring - #375

Merged
POWERFULMOVES merged 5 commits into
mainfrom
feat/centralized-pmoves-ui
Dec 29, 2025
Merged

POWERFULMOVES merged 5 commits into
mainfrom
feat/centralized-pmoves-ui

Conversation

@POWERFULMOVES

@POWERFULMOVES POWERFULMOVES commented Dec 29, 2025

Copy link
Copy Markdown
Owner

Summary

Centralized PMOVES UI with:

  • Real-time health monitoring dashboard
  • Unified navigation across all PMOVES services
  • Integrated service status indicators
  • Consolidated user interface

Test plan

  • Verify health dashboard displays all services
  • Test navigation between service panels
  • Confirm real-time status updates

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

New Features

  • Redesigned Services dashboard with real-time health monitoring and tier-based filtering.
  • Added system health metrics display showing service status overview.
  • Introduced search functionality across service catalog.
  • Added health check endpoints for monitoring service availability.

Bug Fixes

  • Fixed GitHub Actions workflow permissions configuration.
  • Corrected submodule path configuration.

Chores

  • Expanded and reorganized Makefile with additional orchestration targets.
  • Updated automation documentation.

✏️ Tip: You can customize this high-level summary in your review settings.

Codex Agent and others added 4 commits December 27, 2025 00:18
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>
@coderabbitai

coderabbitai Bot commented Dec 29, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
GitHub Actions Workflow Permissions
\.github/workflows/chit-contract\.yml, \.github/workflows/python-tests\.yml, \.github/workflows/webhook-smoke\.yml
Added explicit permissions: {contents: read} block to three workflows to restrict GitHub Actions permissions.
Git Configuration
\.gitmodules
Reorganized submodule path from nested pmoves/pmoves/vendor/e2b to flatter pmoves/vendor/e2b, while preserving URL.
Build Orchestration
pmoves/Makefile
Introduced SCRIPTS variable and 30+ new public targets spanning startup orchestration (up-all-new, up-core, up-minimal), graceful shutdown (down-all, down-integrations, etc.), health/readiness checks (wait-obs, wait-data, wait-workers, wait-agents, health-summary), test suites (test-smoke variants), and integration setup (setup-all-integrations, per-service setup targets).
Service Catalog Core
pmoves/ui/lib/serviceCatalog\.ts, pmoves/ui/lib/serviceHealth\.ts, pmoves/ui/lib/useServiceHealth\.ts
Established comprehensive service catalog with 94+ service definitions, metadata, and endpoints; implemented health probing, concurrent batch checking, and polling-based React hook for real-time health monitoring with status tracking and interval management.
Health/Services API Routes
pmoves/ui/app/api/health-all/route\.ts, pmoves/ui/app/api/services-hub/route\.ts, pmoves/ui/app/api/services/route\.ts
Created three new Next.js Edge API routes for service catalog queries (filtering, search), aggregated health status with tier stats and critical-down detection, and multi-service health checks with optional caching.
Service UI Components
pmoves/ui/components/services/*
Added ServiceCard, ServiceGrid, CategorySection, ServiceHealthIndicator, ServiceHealthBadge, TierNavigation, TierOverview components; exported via index barrel. Components support health status visualization, filtering, searching, tier-based categorization, and expandable tier summaries.
Hub & Dashboard Components
pmoves/ui/components/hub/SystemHubSection\.tsx, pmoves/ui/components/hub/SystemStatsBar\.tsx
Created new SystemHubSection component for real-time service hub dashboard with polling integration; added SystemStatsBar for displaying aggregate health metrics, service counts, and refresh controls.
Page & Layout Refactoring
pmoves/ui/app/page\.tsx, pmoves/ui/app/dashboard/services/page\.tsx
Removed Module/ServiceLink types and probing logic from home page; replaced ModulesSection with new SystemHubSection. Redesigned services dashboard to use dynamic SERVICE_CATALOG-driven rendering, real-time health polling, tier filtering, search functionality, and per-service health indicators.
Test Updates
pmoves/ui/__tests__/services-pages\.test\.tsx, pmoves/ui/e2e/services\.spec\.ts
Updated page title assertions from "Integration Services" to "Services"; refactored services test to use hardcoded sample services from SERVICE_CATALOG; reorganized E2E test service list and added DOCUMENTED_SERVICES filtering.

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
Loading
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
Loading

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

🐰 A rabbit's ode to service harmony

Through catalogs of ninety-four, we hop and bound,
Health pulses steady, round and round,
Dashboards glow with metrics bright,
As services dance in green light,
Make targets spring forth, neat and tight! 🐇

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. It lacks the required Testing section with commands/output and does not include the Required Checks section from the template, which mandates CHIT Contract and documentation checks. Add a Testing section with reproduction steps and commands used to verify the changes. Complete the Required Checks section, marking or explaining the status of CHIT Contract Check, contract/schema updates, and documentation updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(ui): Centralized PMOVES UI with real-time health monitoring' directly summarizes the main change: a centralized UI dashboard with health monitoring capabilities across PMOVES services.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/centralized-pmoves-ui

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +75 to +80
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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +763 to +770
slug: 'invidious',
title: 'Invidious',
summary: 'Privacy-focused YouTube frontend',
category: 'integration',
color: 'cyan',
endpoints: [
{ name: 'Web UI', port: '8095', path: '/', type: 'ui' },
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +98 to +104
const criticalSlugs = [
'prometheus', // Observability foundation
'postgres', // Primary database
'nats', // Message bus
'agent-zero', // Orchestrator
'tensorzero', // LLM gateway
'hi-rag-v2', // Knowledge retrieval

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 escapeRegExp helper 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 parseInt call doesn't validate the result, which could lead to NaN being passed to checkAllServices if 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: boolean field, 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 alignment

The new orchestration flow (up-obsup-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 standard all/test targets are also a nice DX win.

A couple of things to double-check:

  • test-smoke-parallel uses pytest -n auto, which requires pytest-xdist in the environment. Make sure that’s included in the relevant requirements so this target doesn’t fail unexpectedly.
  • up-agents and up-tensorzero are 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, and docs/LOCAL_CI_CHECKS.md for any older make smoke/up-all references and update them to mention make -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 scripts

The new SCRIPTS := scripts var and setup-* 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-*.sh files exist under pmoves/scripts/.
  • They’re executable (chmod +x) and behave correctly when invoked with the status subcommand.
  • 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 smoke

The 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.md guidelines for pmoves/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 unused collapsed prop

The category header, health summary, and ServiceCard wiring all look consistent with ServiceCategory and ServiceHealthMap, and the health counts are computed correctly.

The only minor nit is that collapsed?: boolean is defined on CategorySectionProps but 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 for category parameter.

The category is cast directly to ServiceCategory | null without validation. If a client sends an invalid category string, getServicesByCategory will 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 slug comparison on line 61 doesn't apply toLowerCase(), unlike title and summary. 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: Unused statusBg variable.

The statusBg variable 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 date is slightly in the future (clock skew), diff would 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-hub appears in both useEffect and handleRefresh. 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, calculating tierStats, and computing overallStats). 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 using animate-pulse instead of animate-spin for checking state.

Since the indicator is a circle, animate-spin rotation is not visually perceptible. animate-pulse would 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 reusing getStatusText from serviceHealth.ts.

The statusText mapping duplicates the logic in getStatusText from @/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: Redundant mountedRef.current = true assignment.

Line 127 sets mountedRef.current = true, but it's already initialized to true on 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0f82834 and 76a181d.

📒 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
  • .gitmodules
  • pmoves/Makefile
  • pmoves/ui/__tests__/services-pages.test.tsx
  • pmoves/ui/app/api/health-all/route.ts
  • pmoves/ui/app/api/services-hub/route.ts
  • pmoves/ui/app/api/services/route.ts
  • pmoves/ui/app/dashboard/services/page.tsx
  • pmoves/ui/app/page.tsx
  • pmoves/ui/components/hub/SystemHubSection.tsx
  • pmoves/ui/components/hub/SystemStatsBar.tsx
  • pmoves/ui/components/services/CategorySection.tsx
  • pmoves/ui/components/services/ServiceCard.tsx
  • pmoves/ui/components/services/ServiceGrid.tsx
  • pmoves/ui/components/services/ServiceHealthIndicator.tsx
  • pmoves/ui/components/services/ServicesHeaderActions.tsx
  • pmoves/ui/components/services/TierNavigation.tsx
  • pmoves/ui/components/services/TierOverview.tsx
  • pmoves/ui/components/services/index.ts
  • pmoves/ui/e2e/services.spec.ts
  • pmoves/ui/lib/serviceCatalog.ts
  • pmoves/ui/lib/serviceHealth.ts
  • pmoves/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; reference pmoves/docs/UI_NOTEBOOK_WORKBENCH.md

Files:

  • pmoves/ui/components/services/index.ts
  • pmoves/ui/components/services/CategorySection.tsx
  • pmoves/ui/lib/serviceCatalog.ts
  • pmoves/ui/components/services/ServiceGrid.tsx
  • pmoves/ui/app/api/health-all/route.ts
  • pmoves/ui/__tests__/services-pages.test.tsx
  • pmoves/ui/app/api/services-hub/route.ts
  • pmoves/ui/components/services/ServiceCard.tsx
  • pmoves/ui/components/services/ServicesHeaderActions.tsx
  • pmoves/ui/components/hub/SystemStatsBar.tsx
  • pmoves/ui/app/dashboard/services/page.tsx
  • pmoves/ui/components/services/ServiceHealthIndicator.tsx
  • pmoves/ui/components/hub/SystemHubSection.tsx
  • pmoves/ui/app/api/services/route.ts
  • pmoves/ui/lib/serviceHealth.ts
  • pmoves/ui/components/services/TierNavigation.tsx
  • pmoves/ui/app/page.tsx
  • pmoves/ui/components/services/TierOverview.tsx
  • pmoves/ui/lib/useServiceHealth.ts
  • pmoves/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.ts
  • pmoves/ui/__tests__/services-pages.test.tsx
  • pmoves/ui/lib/serviceHealth.ts
  • pmoves/ui/e2e/services.spec.ts
  • 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: 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.ts
  • pmoves/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.tsx
  • pmoves/ui/e2e/services.spec.ts
  • 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/{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.tsx
  • pmoves/ui/e2e/services.spec.ts
  • 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/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.tsx
  • pmoves/ui/app/page.tsx
  • pmoves/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.md
  • 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/*/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.ts
  • 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: 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.ts
  • 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/*/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: read permission 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) and DOCUMENTED_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 ServiceCategory union type from serviceCatalog.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 tooling

This 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/Makefile and 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=10 and stale-while-revalidate=30 is 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 percentage keeps the API response informative while leveraging the existing HealthCheckResult structure.

pmoves/ui/components/hub/SystemHubSection.tsx (1)

128-146: Good composition of health components.

The SystemStatsBar receives computed stats and the TierOverviewGrid uses the health map for per-service status. The tier expand toggle logic is clean.

pmoves/ui/components/services/ServiceCard.tsx (2)

50-51: Hardcoded localhost in 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), while ServiceCard.tsx uses http://localhost:${port} (direct service access). Ensure this difference is intentional—this page navigates to detail views while ServiceCard may 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-label attribute 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 ServiceHealthMap with 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.

Comment thread .gitmodules
Comment on lines +99 to +100
[submodule "pmoves/vendor/e2b"]
path = pmoves/vendor/e2b

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Repository: 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)"
fi

Repository: 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:

  1. Move the directory from pmoves/pmoves/vendor/e2b to pmoves/vendor/e2b
  2. Reinitialize the submodule to register it with git: git submodule update --init --recursive
  3. 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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> = {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +84 to +95
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>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +31 to +50
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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "ServiceGrid.tsx" -type f

Repository: 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'
fi

Repository: 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"
fi

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 116


🏁 Script executed:

# Search for ServiceGrid in the codebase to confirm location
fd "ServiceGrid" --type f

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 112


🏁 Script executed:

# Search for Service type definition
rg "interface Service|type Service" --type ts --type tsx -A 5

Repository: 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 -50

Repository: 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 1

Repository: 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.

Comment on lines +151 to +170
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);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "TierOverview.tsx" -type f

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 115


🏁 Script executed:

cat -n pmoves/ui/components/services/TierOverview.tsx | head -200

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 6373


🏁 Script executed:

cat -n pmoves/ui/components/services/TierOverview.tsx | tail -n +170 | head -150

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 5839


🏁 Script executed:

cat -n pmoves/ui/components/services/TierOverview.tsx | tail -n +315 | head -20

Repository: 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).

Comment on lines +432 to +442
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'],
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's confirm the presign service definition in serviceCatalog.ts
cd pmoves && head -450 ui/lib/serviceCatalog.ts | tail -50

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 190


🏁 Script executed:

# Search the Makefile for presign health check references
cd pmoves && grep -n "presign" Makefile

Repository: 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/null

Repository: 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 -60

Repository: 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.ts

Repository: 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.ts

Repository: 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.

Suggested change
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.

Comment on lines +39 to +56
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +72 to +84
// 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');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

Comment on lines +103 to +121
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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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
@POWERFULMOVES
POWERFULMOVES merged commit 4bd5c1d into main Dec 29, 2025
4 checks passed
POWERFULMOVES added a commit that referenced this pull request Jan 1, 2026
* 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>
POWERFULMOVES added a commit that referenced this pull request Jan 18, 2026
* 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>
@POWERFULMOVES
POWERFULMOVES deleted the feat/centralized-pmoves-ui branch March 7, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant