diff --git a/.claude/learnings/pr366-self-hosted-runner-2025-12.md b/.claude/learnings/pr366-self-hosted-runner-2025-12.md index 5d80125305..2f052e921d 100644 --- a/.claude/learnings/pr366-self-hosted-runner-2025-12.md +++ b/.claude/learnings/pr366-self-hosted-runner-2025-12.md @@ -14,117 +14,10 @@ Migrated 5 GitHub Actions workflows from `ubuntu-latest` to self-hosted runners ### 1. GitHub Actions Permissions -**Pattern:** Always include explicit `permissions:` blocks in workflows. - -**Fixed Files:** -- `.github/workflows/chit-contract.yml` -- `.github/workflows/python-tests.yml` -- `.github/workflows/webhook-smoke.yml` - -**Change:** -```yaml -# After `on:` section, before `jobs:`: -permissions: - contents: read -``` - -**Why:** GitHub Actions defaults to `write-all` permissions for backward compatibility. Explicitly declaring `contents: read` follows the principle of least privilege and prevents workflows from accidentally having repository write access. - -**Reference:** [GitHub Actions - Default permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) - ---- - -## Out-of-Diff Fixes (Technical Debt) - -### 2. Git Submodule Path Duplication - -**File:** `.gitmodules` - -**Problem:** Path duplication in e2b submodule: -```ini -[submodule "pmoves/pmoves/vendor/e2b"] # WRONG - path = pmoves/pmoves/vendor/e2b # WRONG -``` - -**Fixed:** -```ini -[submodule "pmoves/vendor/e2b"] # CORRECT - path = pmoves/vendor/e2b # CORRECT -``` - -**Why:** When adding submodules manually, it's easy to duplicate path components. The correct approach is to use `git submodule add` CLI which validates paths automatically. - -**Lesson:** Never edit `.gitmodules` manually. Use: -```bash -git submodule add -``` - ---- - -### 3. Makefile Working Directory Context - -**File:** `pmoves/Makefile` - -**Problem:** Test-smoke targets used `cd pmoves && pytest ...` -```makefile -test-smoke: - cd pmoves && pytest tests/smoke/ -v -m smoke -``` - -**Why it fails:** When invoked via `make -C pmoves`, the working directory is already `pmoves/`. The `cd pmoves` tries to change into `pmoves/pmoves/` which doesn't exist. - -**Fixed:** -```makefile -test-smoke: - pytest tests/smoke/ -v -m smoke -``` - -**Lesson:** When using `make -C `, never use `cd ` in recipes. The working directory is already set to ``. - ---- - -### 4. Makefile Undefined Variables - -**File:** `pmoves/Makefile` - -**Problem:** `$(SCRIPTS)` variable was used but not defined. - -**Fixed:** Added at line 44: -```makefile -PYTHON ?= python3 -SCRIPTS := scripts -SINGLE_ENV_MODE ?= 1 -``` - -**Lesson:** Define all Makefile variables before use. Group related variable definitions together near the top of the file. - ---- - -### 5. Makefile Standard Targets - -**File:** `pmoves/Makefile` - -**Problem:** Missing conventional `all` and `test` targets. - -**Fixed:** Added after line 70: -```makefile -# -------- Standard Makefile targets ---------- -.PHONY: all -all: help ## Default target - show help - -.PHONY: test -test: test-smoke ## Run pytest smoke tests - -.DEFAULT_GOAL := help -``` - -**Lesson:** POSIX Makefiles should include: -- `all` - default target (should show help) -- `test` - alias to project's test suite -- `clean` - remove build artifacts (already existed) -- `.DEFAULT_GOAL` - explicit default behavior - ---- +1. **GitHub Actions Permissions:** Always include explicit `permissions:` blocks for least privilege +2. **Git Submodules:** Use `git submodule add` CLI, never edit `.gitmodules` manually +3. **Makefile `make -C`:** Never use `cd` in recipes when using `make -C ` +4. **Makefile Standards:** Include `all`, `test`, `clean` targets and `.DEFAULT_GOAL` ## Runner Labels Reference @@ -133,8 +26,6 @@ test: test-smoke ## Run pytest smoke tests | CPU (VPS) | `[self-hosted, vps]` | General CI, Python tests, contract checks | | GPU (AI Lab) | `[self-hosted, ai-lab, gpu]` | ML models, CUDA workloads, TTS | ---- - ## Pre-Merge Checklist - [x] All workflows have `permissions: contents: read` @@ -144,8 +35,6 @@ test: test-smoke ## Run pytest smoke tests - [ ] CI passes on self-hosted runners - [ ] Smoke tests pass locally ---- - ## Commands for Validation ```bash @@ -162,8 +51,6 @@ grep "^path=" .gitmodules | sort | uniq -d cd pmoves && make test-smoke ``` ---- - ## References - [GitHub Actions Security](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) diff --git a/.github/workflows/chit-contract.yml b/.github/workflows/chit-contract.yml index 089f200cef..c131b53f0c 100644 --- a/.github/workflows/chit-contract.yml +++ b/.github/workflows/chit-contract.yml @@ -20,6 +20,9 @@ on: - 'pmoves/docs/SUPABASE_*.md' - '.github/workflows/chit-contract.yml' +permissions: + contents: read + jobs: verify: runs-on: ubuntu-latest diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index efbdc90ec0..5596defec8 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -22,6 +22,12 @@ on: - 'pmoves/tests/**' - '.github/workflows/python-tests.yml' +permissions: + contents: read + +permissions: + contents: read + jobs: tests: runs-on: ubuntu-latest diff --git a/.github/workflows/webhook-smoke.yml b/.github/workflows/webhook-smoke.yml index 3b8ebd10a2..ecc2c52edd 100644 --- a/.github/workflows/webhook-smoke.yml +++ b/.github/workflows/webhook-smoke.yml @@ -14,6 +14,9 @@ on: required: false default: "false" +permissions: + contents: read + jobs: smoke: runs-on: ubuntu-latest diff --git a/pmoves/Makefile b/pmoves/Makefile index 4ed880cb28..018d82d0b9 100644 --- a/pmoves/Makefile +++ b/pmoves/Makefile @@ -30,6 +30,7 @@ COMPOSE_PROFILES ?= $(compose_profiles)$(neo4j_profile)$(meili_profile)$(qdrant_ export COMPOSE_PROFILES PYTHON ?= python3 +SCRIPTS := scripts SINGLE_ENV_MODE ?= 1 BOOTSTRAP_FLAGS ?= ENV_SHARED_FILE ?= env.shared @@ -65,6 +66,16 @@ ensure-env-shared: fi; \ fi +# -------- Standard Makefile targets ---------- +.PHONY: all +all: help ## Default target - show help + +.PHONY: test +test: test-smoke ## Run pytest smoke tests + +.DEFAULT_GOAL := help + +# -------- Updates ---------- .PHONY: update-service-docs update-service-docs: ## Regenerate service update notes from git metadata @$(PYTHON) scripts/update_service_logs.py $(ARGS) @@ -77,6 +88,287 @@ update: ensure-env-shared ## Pull repo + images, recreate stack @docker compose up -d @echo "βœ” Updated & reconciled containers." +# One-shot bring-up for the common local dev stack (keeps container names stable under the $(PROJECT) project). +.PHONY: up-all +up-all: ## Start core, agents+UIs, n8n, and monitoring (single env) + @$(MAKE) up + @$(MAKE) up-agents-ui + @$(MAKE) up-bots + @$(MAKE) up-external-on + @$(MAKE) up-n8n + @$(MAKE) up-monitoring + @echo "βœ” Full stack up (core + agents UI + n8n + monitoring)." + +.PHONY: up-bots +up-bots: ## Start bot services (BotZ + messaging gateway) + @$(DC) --profile data --profile workers --profile botz up -d botz-gateway messaging-gateway + @echo "βœ” Bots up (botz-gateway + messaging-gateway)." + +# ============================================================================= +# OBSERVABILITY-FIRST STARTUP (Tier-based, dependency-ordered) +# ============================================================================= + +.PHONY: up-obs +up-obs: ## Start observability stack FIRST (Prometheus, Grafana, Loki, Promtail, cAdvisor) + @echo "πŸ“Š Starting observability stack (monitoring FIRST)..." + @$(LOAD_ENV_SHARED) docker compose -p $(PROJECT) -f docker-compose.yml --profile monitoring up -d + @echo "⏳ Waiting for observability to be ready..." + @timeout 60 bash -c 'until curl -sf http://localhost:9090/-/ready; do sleep 2; done' || echo "⚠️ Prometheus not ready (may still be starting)" + @timeout 60 bash -c 'until curl -sf http://localhost:3002/api/health; do sleep 2; done' || echo "⚠️ Grafana not ready (may still be starting)" + @echo "βœ… Observability ready - capturing all logs from here on" + @echo " Grafana: http://localhost:3002 (admin/admin)" + @echo " Prometheus: http://localhost:9090" + @echo " Loki: http://localhost:3100" + +.PHONY: up-supabase +up-supabase: ## Start Supabase (Postgres + Kong + Studio) + @echo "πŸ—„οΈ Starting Supabase..." + @cd .. && supabase start --network-id pmoves-net + @echo "βœ… Supabase ready" + @echo " Studio: http://localhost:65433" + +.PHONY: up-data-tier +up-data-tier: ## Start data tier (Qdrant, Neo4j, Meilisearch, MinIO) + @echo "πŸ’Ύ Starting data tier..." + @$(DC) --profile data up -d + @$(MAKE) --no-print-directory wait-data + @echo "βœ… Data tier ready" + +.PHONY: up-bus +up-bus: ## Start message bus (NATS) + @echo "πŸ“¨ Starting message bus (NATS)..." + @$(DC) up -d nats + @timeout 30 bash -c 'until docker exec pmoves-nats-1 nc -z localhost 4222 2>/dev/null; do sleep 1; done' || echo "⚠️ NATS may still be starting" + @echo "βœ… NATS ready" + +.PHONY: up-workers +up-workers: ## Start worker services (extract, langextract, media) + @echo "βš™οΈ Starting worker services..." + @$(DC) --profile workers up -d + @$(MAKE) --no-print-directory wait-workers + @echo "βœ… Workers ready" + +.PHONY: up-agents +up-agents: ## Start agent services (Agent Zero, Archon, DeepResearch, SupaSerch) + @echo "πŸ€– Starting agent services..." + @$(DC) --profile agents up -d + @$(MAKE) --no-print-directory wait-agents + @echo "βœ… Agents ready" + +.PHONY: up-tensorzero +up-tensorzero: ## Start TensorZero LLM gateway (PRIMARY MODEL PROVIDER) + @echo "🧠 Starting TensorZero LLM gateway..." + @$(DC) --profile tensorzero up -d + @timeout 60 bash -c 'until curl -sf http://localhost:3030/healthz; do sleep 2; done' || echo "⚠️ TensorZero may still be starting" + @echo "βœ… TensorZero ready - LLM calls available" + @echo " Gateway: http://localhost:3030" + @echo " UI: http://localhost:4000" + +.PHONY: up-integrations +up-integrations: ## Start external integrations (n8n, TTS) + @echo "πŸ”— Starting external integrations..." + @$(MAKE) up-n8n || true + @echo "βœ… Integrations started" + +.PHONY: up-ui +up-ui: ## Start PMOVES UI (centralized dashboard at port 4482) + @echo "πŸ–₯️ Starting PMOVES UI..." + @$(DC) --profile ui up -d pmoves-ui + @timeout 60 bash -c 'until curl -sf http://localhost:4482/api/health; do sleep 2; done' || echo "⚠️ UI may still be starting" + @echo "βœ… PMOVES UI ready" + @echo " Dashboard: http://localhost:4482" + @echo " Services: http://localhost:4482/dashboard/services" + +# ============================================================================= +# HIGH-LEVEL STARTUP TARGETS (Observability-First Order) +# ============================================================================= + +.PHONY: up-all-new +up-all-new: ## Start ALL services in dependency order (obs first, then data, bus, workers, agents, tensorzero, ui) + @$(MAKE) up-obs + @$(MAKE) up-supabase + @$(MAKE) up-data-tier + @$(MAKE) up-bus + @$(MAKE) up-workers + @$(MAKE) up-agents + @$(MAKE) up-tensorzero + @$(MAKE) up-integrations + @$(MAKE) up-ui + @echo "βœ… ALL PMOVES services started" + @$(MAKE) --no-print-directory status-all + +.PHONY: up-core +up-core: ## Start core services (obs + supabase + data + bus + workers + agents, no tensorzero/integrations) + @$(MAKE) up-obs + @$(MAKE) up-supabase + @$(MAKE) up-data-tier + @$(MAKE) up-bus + @$(MAKE) up-workers + @$(MAKE) up-agents + @echo "βœ… Core PMOVES services started (no TensorZero/integrations)" + +.PHONY: up-minimal +up-minimal: ## Start minimal stack (Supabase + Data + Bus only) + @$(MAKE) up-supabase + @$(MAKE) up-data-tier + @$(MAKE) up-bus + @echo "βœ… Minimal stack ready (Supabase + Data + Bus)" + +# ============================================================================= +# GRACEFUL SHUTDOWN (Reverse Dependency Order) +# ============================================================================= + +.PHONY: down-all +down-all: ## Stop ALL services in graceful reverse order + @echo "πŸ›‘ Graceful shutdown starting..." + @-$(MAKE) --no-print-directory down-integrations 2>/dev/null + @-$(MAKE) --no-print-directory down-tensorzero + @-$(MAKE) --no-print-directory down-agents + @-$(MAKE) --no-print-directory down-workers + @-$(MAKE) --no-print-directory down-bus + @-$(MAKE) --no-print-directory down-data + @-$(MAKE) --no-print-directory down-obs + @-$(MAKE) --no-print-directory down-supabase + @echo "βœ… All services stopped gracefully" + +.PHONY: down-integrations +down-integrations: ## Stop external integrations + @echo "πŸ”— Stopping integrations..." + @-$(MAKE) down-n8n 2>/dev/null || true + +.PHONY: down-tensorzero +down-tensorzero: ## Stop TensorZero gateway + @echo "🧠 Stopping TensorZero..." + @$(DC) --profile tensorzero down + +.PHONY: down-agents +down-agents: ## Stop agent services + @echo "πŸ€– Stopping agents..." + @$(DC) --profile agents down + +.PHONY: down-workers +down-workers: ## Stop worker services + @echo "βš™οΈ Stopping workers..." + @$(DC) --profile workers down + +.PHONY: down-bus +down-bus: ## Stop message bus + @echo "πŸ“¨ Stopping NATS..." + @$(DC) stop nats || true + @$(DC) rm -f nats || true + +.PHONY: down-data +down-data: ## Stop data tier + @echo "πŸ’Ύ Stopping data tier..." + @$(DC) --profile data down + +.PHONY: down-obs +down-obs: ## Stop observability stack + @echo "πŸ“Š Stopping observability..." + @$(LOAD_ENV_SHARED) docker compose -p $(PROJECT) -f docker-compose.yml --profile monitoring down + +.PHONY: down-ui +down-ui: ## Stop PMOVES UI + @echo "πŸ–₯️ Stopping PMOVES UI..." + @$(DC) --profile ui down pmoves-ui + +.PHONY: down-supabase +down-supabase: ## Stop Supabase + @echo "πŸ—„οΈ Stopping Supabase..." + @cd .. && supabase stop + +# ============================================================================= +# STATUS & HEALTH TARGETS +# ============================================================================= + +.PHONY: status-all +status-all: ## Show health status of ALL services + @echo "πŸ“Š PMOVES Service Status" + @echo "=======================" + @echo "" + @echo "πŸ” OBSERVABILITY:" + @docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null | grep -E "(prometheus|grafana|loki|promtail|cadvisor|NAMES)" || echo " (none running)" + @echo "" + @echo "πŸ—„οΈ SUPABASE:" + @docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null | grep supabase || echo " (not running - use 'make up-supabase')" + @echo "" + @echo "πŸ’Ύ DATA TIER:" + @docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null | grep -E "(qdrant|neo4j|meilisearch|minio)" || echo " (none running)" + @echo "" + @echo "πŸ“¨ BUS:" + @docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null | grep nats || echo " (none running)" + @echo "" + @echo "βš™οΈ WORKERS:" + @docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null | grep -E "(extract|langextract|media|pdf-ingest|notebook-sync|presign|render-webhook)" || echo " (none running)" + @echo "" + @echo "πŸ€– AGENTS:" + @docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null | grep -E "(agent-zero|archon|deepresearch|supaserch)" || echo " (none running)" + @echo "" + @echo "🧠 TENSORZERO:" + @docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null | grep tensorzero || echo " (none running)" + @echo "" + @$(MAKE) --no-print-directory health-summary + +.PHONY: health-summary +health-summary: ## Run quick health check on all services + @echo "πŸ₯ Health Summary:" + @python3 tools/flight_check_retro.py || true + @echo "" + @echo "πŸ“ Full results saved to .validation/" + +# ============================================================================= +# WAIT TARGETS (Dependency Satisfaction) +# ============================================================================= + +.PHONY: wait-obs +wait-obs: ## Wait for observability to be ready + @echo "⏳ Waiting for observability..." + @timeout 60 bash -c 'until curl -sf http://localhost:9090/-/ready; do sleep 2; done' || true + @timeout 60 bash -c 'until curl -sf http://localhost:3002/api/health; do sleep 2; done' || true + @echo "βœ… Observability ready" + +.PHONY: wait-data +wait-data: ## Wait for data tier to be ready + @echo "⏳ Waiting for data tier..." + @timeout 60 bash -c 'until curl -sf http://localhost:6333/ready; do sleep 2; done' || true + @timeout 60 bash -c 'until curl -sf http://localhost:7474; do sleep 2; done' || true + @timeout 60 bash -c 'until curl -sf http://localhost:7700/health; do sleep 2; done' || true + @timeout 60 bash -c 'until curl -sf http://localhost:9000/minio/health/live; do sleep 2; done' || true + @echo "βœ… Data tier ready" + +.PHONY: wait-workers +wait-workers: ## Wait for workers to be ready + @echo "⏳ Waiting for workers..." + @timeout 60 bash -c 'until curl -sf http://localhost:8083/healthz; do sleep 2; done' || true + @timeout 60 bash -c 'until curl -sf http://localhost:8084/healthz; do sleep 2; done' || true + @echo "βœ… Workers ready" + +.PHONY: wait-agents +wait-agents: ## Wait for agents to be ready + @echo "⏳ Waiting for agents..." + @timeout 60 bash -c 'until curl -sf http://localhost:8080/healthz; do sleep 2; done' || true + @timeout 60 bash -c 'until curl -sf http://localhost:8091/healthz; do sleep 2; done' || true + @timeout 60 bash -c 'until curl -sf http://localhost:8098/healthz; do sleep 2; done' || true + @echo "βœ… Agents ready" + +# ============================================================================= +# INVENTORY & VALIDATION TARGETS +# ============================================================================= + +.PHONY: inventory +inventory: ## List all running PMOVES services by tier + @echo "πŸ“¦ PMOVES Service Inventory:" + @echo "Data Tier:"; docker ps --format " {{.Names}}" 2>/dev/null | grep -E "(qdrant|neo4j|meilisearch|minio)" || echo " (none running)" + @echo "Worker Tier:"; docker ps --format " {{.Names}}" 2>/dev/null | grep -E "(extract|langextract|media)" || echo " (none running)" + @echo "Agent Tier:"; docker ps --format " {{.Names}}" 2>/dev/null | grep -E "(agent-zero|archon|deepresearch|nats)" || echo " (none running)" + @echo "Monitoring:"; docker ps --format " {{.Names}}" 2>/dev/null | grep -E "(prometheus|grafana|loki)" || echo " (none running)" + +.PHONY: validate-tier +validate-tier: ## Validate tier network compliance (backend services should NOT be on pmoves-net) + @echo "πŸ” Validating tier network compliance..." + @echo "Backend services should NOT be on pmoves-net (except UIs)" + @docker ps --format "table {{.Names}}\t{{.Networks}}" 2>/dev/null | grep pmoves-net | grep -v "supabase\|archon\|agent-zero" || echo "βœ… No unexpected services on pmoves-net" + # -------- PMOVES.YT docs helpers ---------- .PHONY: yt-docs-sync yt-docs-catalog-smoke @@ -1206,6 +1498,35 @@ smoke: channel-monitor-up: docker compose -p $(PROJECT) --profile yt up -d channel-monitor +# ============================================================================ +# PYTEST-BASED SMOKE TESTS (Unified Testing Framework) +# ============================================================================ + +.PHONY: test-smoke +test-smoke: ## Run pytest smoke tests for all services (5-30s) + @echo "Running pytest smoke tests..." + pytest tests/smoke/ -v -m smoke --tb=short + +.PHONY: test-smoke-quick +test-smoke-quick: ## Run pytest smoke tests with minimal output (fail fast) + @echo "Running quick smoke tests..." + pytest tests/smoke/ -m smoke -q --maxfail=5 + +.PHONY: test-smoke-health +test-smoke-health: ## Run pytest smoke tests for health endpoints only + @echo "Testing all service health endpoints..." + pytest tests/smoke/test_health_endpoints.py -v -m smoke + +.PHONY: test-smoke-critical +test-smoke-critical: ## Run pytest smoke tests for critical dependency path + @echo "Testing critical dependency path..." + pytest tests/smoke/test_critical_path.py -v -m smoke + +.PHONY: test-smoke-parallel +test-smoke-parallel: ## Run pytest smoke tests in parallel (faster) + @echo "Running smoke tests in parallel..." + pytest tests/smoke/ -v -m smoke -n auto + .PHONY: channel-monitor-smoke channel-monitor-smoke: @echo "[Channel Monitor] Triggering check..." && curl -sS -X POST http://localhost:8097/api/monitor/check-now | jq -e '.status=="ok"' >/dev/null && echo OK || (echo FAIL && exit 1) @@ -2084,3 +2405,37 @@ build-push-deepresearch: buildx-setup docker-login ## Build+push DeepResearch wo img=$(REGISTRY)/$(IMAGE_NAMESPACE)/pmoves-deepresearch:$(IMAGE_TAG); \ echo "β†’ Building $$img"; \ docker buildx build --platform $(TARGET_PLATFORMS) -f "$$df" -t "$$img" "$$ctx" --push + +# ============================================================================ +# PMOVES Integration Setup Targets +# Added for TAC 2: Auth & Documentation +# ============================================================================ + +.PHONY: setup-all-integrations +setup-all-integrations: ## Run setup for all PMOVES integrations + @echo "πŸ”§ Setting up all PMOVES integrations..." + @$(SCRIPTS)/setup-all-integrations.sh status + +.PHONY: setup-agent-zero +setup-agent-zero: ## Run Agent Zero setup + @$(SCRIPTS)/setup-agent-zero.sh status + +.PHONY: setup-archon +setup-archon: ## Run Archon setup + @$(SCRIPTS)/setup-archon.sh status + +.PHONY: setup-supaserch +setup-supaserch: ## Run SupaSerch setup + @$(SCRIPTS)/setup-supaserch.sh status + +.PHONY: setup-deepresearch +setup-deepresearch: ## Run DeepResearch setup + @$(SCRIPTS)/setup-deepresearch.sh status + +.PHONY: setup-flute-gateway +setup-flute-gateway: ## Run Flute Gateway setup + @$(SCRIPTS)/setup-flute-gateway.sh status + +.PHONY: setup-extract-worker +setup-extract-worker: ## Run Extract Worker setup + @$(SCRIPTS)/setup-extract-worker.sh status diff --git a/pmoves/ui/__tests__/services-pages.test.tsx b/pmoves/ui/__tests__/services-pages.test.tsx index 70cb070b52..548af8d24c 100644 --- a/pmoves/ui/__tests__/services-pages.test.tsx +++ b/pmoves/ui/__tests__/services-pages.test.tsx @@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react'; import ServicesIndexPage from '@/app/dashboard/services/page'; import ServiceDetailPage from '@/app/dashboard/services/[service]/page'; import { INTEGRATION_SERVICES } from '@/lib/services'; +import { SERVICE_CATALOG } from '@/lib/serviceCatalog'; import { notFound } from 'next/navigation'; jest.mock('react-markdown', () => ({ @@ -32,14 +33,17 @@ describe('Services dashboards', () => { it('lists all operator integrations on the index route', () => { render(); + // TAC 1: The centralized UI uses "Services" as the page title expect( - screen.getByRole('heading', { name: /integration services/i }) + screen.getByRole('heading', { name: /services/i }) ).toBeInTheDocument(); - INTEGRATION_SERVICES.forEach((service) => { - expect( - screen.getByRole('link', { name: new RegExp(service.title, 'i') }) - ).toBeInTheDocument(); + // TAC 1: The new centralized UI displays services from SERVICE_CATALOG + // Check that a sample of key services are present + const sampleServices = ['Prometheus', 'Grafana', 'Agent Zero', 'Archon', 'TensorZero']; + sampleServices.forEach((title) => { + const links = screen.getAllByRole('link', { name: new RegExp(title, 'i') }); + expect(links.length).toBeGreaterThan(0); }); }); diff --git a/pmoves/ui/app/dashboard/services/page.tsx b/pmoves/ui/app/dashboard/services/page.tsx index 4f0b1c1e0e..9e9017e4a9 100644 --- a/pmoves/ui/app/dashboard/services/page.tsx +++ b/pmoves/ui/app/dashboard/services/page.tsx @@ -1,48 +1,273 @@ +'use client'; + +import { useState, useMemo } from 'react'; import Link from 'next/link'; -import type { Metadata } from 'next'; -import DashboardNavigation from '../../../components/DashboardNavigation'; -import { INTEGRATION_SERVICES } from '../../../lib/services'; - -export const metadata: Metadata = { - title: 'Integration services | PMOVES Console', - description: - 'Browse the PMOVES operator integrations including Open Notebook, PMOVES.YT, Jellyfin, Wger, and Firefly.', +import { DashboardShell } from '../../../components/DashboardNavigation'; +import { SystemStatsBar } from '../../../components/hub/SystemStatsBar'; +import { TierNavigation } from '../../../components/services/TierNavigation'; +import { ServiceHealthIndicator } from '../../../components/services/ServiceHealthIndicator'; +import { useServiceHealth } from '../../../lib/useServiceHealth'; +import { SERVICE_CATALOG, type ServiceCategory, type ServiceColor } from '../../../lib/serviceCatalog'; +import type { ServiceHealthMap } from '../../../lib/serviceHealth'; + +// Lookup objects for Tailwind JIT - all class names must be statically analyzable +const TAG_CLASSES: Record = { + cyan: 'tag tag-cyan', + ember: 'tag tag-ember', + violet: 'tag tag-violet', + forest: 'tag tag-forest', + gold: 'tag tag-gold', +}; + +const ICON_BG_CLASSES: Record = { + cyan: 'bg-cata-cyan/10 text-cata-cyan', + ember: 'bg-cata-ember/10 text-cata-ember', + violet: 'bg-cata-violet/10 text-cata-violet', + forest: 'bg-cata-forest/10 text-cata-forest', + gold: 'bg-cata-gold/10 text-cata-gold', }; +const BORDER_CLASSES: Record = { + cyan: 'border-cata-cyan/30 hover:border-cata-cyan group-hover:text-cata-cyan', + ember: 'border-cata-ember/30 hover:border-cata-ember group-hover:text-cata-ember', + violet: 'border-cata-violet/30 hover:border-cata-violet group-hover:text-cata-violet', + forest: 'border-cata-forest/30 hover:border-cata-forest group-hover:text-cata-forest', + gold: 'border-cata-gold/30 hover:border-cata-gold group-hover:text-cata-gold', +}; + +const PORT_LINKS = [ + { name: 'TensorZero UI', port: '4000', href: 'http://localhost:4000' }, + { name: 'Grafana', port: '3000', href: 'http://localhost:3000' }, + { name: 'Prometheus', port: '9090', href: 'http://localhost:9090' }, + { name: 'Supabase Studio', port: '65433', href: 'http://127.0.0.1:65433' }, + { name: 'Agent Zero UI', port: '8081', href: 'http://localhost:8081' }, + { name: 'Archon UI', port: '3737', href: 'http://localhost:3737' }, + { name: 'Jellyfin', port: '8096', href: 'http://localhost:8096' }, + { name: 'MinIO Console', port: '9001', href: 'http://localhost:9001' }, +]; + +/** + * Services Dashboard with full catalog, tier filtering, and health monitoring + */ export default function ServicesIndexPage() { + const [activeTier, setActiveTier] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + + const { health, isPolling, lastUpdate, refresh } = useServiceHealth({ + pollInterval: 30000, + enabled: true, + }); + + // Filter services by tier and search query + const filteredServices = useMemo(() => { + return SERVICE_CATALOG.filter((service) => { + const matchesTier = activeTier === 'all' || service.category === activeTier; + const matchesSearch = searchQuery === '' || + service.title.toLowerCase().includes(searchQuery.toLowerCase()) || + service.summary.toLowerCase().includes(searchQuery.toLowerCase()) || + service.slug.toLowerCase().includes(searchQuery.toLowerCase()); + return matchesTier && matchesSearch; + }); + }, [activeTier, searchQuery]); + + // Calculate tier stats for navigation + const tierStats = useMemo(() => { + const stats: Record = {}; + + for (const service of SERVICE_CATALOG) { + const tier = service.category; + if (!stats[tier]) { + stats[tier] = { total: 0, healthy: 0, percentage: 0 }; + } + stats[tier].total++; + if (health[service.slug]?.status === 'healthy') { + stats[tier].healthy++; + } + } + + // Calculate percentages + for (const tier in stats) { + stats[tier].percentage = stats[tier].total > 0 + ? Math.round((stats[tier].healthy / stats[tier].total) * 100) + : 0; + } + + return Object.entries(stats).map(([tier, data]) => ({ + tier: tier as ServiceCategory, + total: data.total, + healthy: data.healthy, + percentage: data.percentage, + })); + }, [health]); + + // Calculate overall stats + const overallStats = useMemo(() => { + const total = SERVICE_CATALOG.length; + const healthy = Object.values(health).filter(h => h.status === 'healthy').length; + const unhealthy = Object.values(health).filter(h => h.status === 'unhealthy').length; + const unknown = total - healthy - unhealthy; + const percentage = total > 0 ? Math.round((healthy / total) * 100) : 0; + + return { total, healthy, unhealthy, unknown, percentage }; + }, [health]); + return ( -
- -
-

Integration services

-
-

- Quick links to the external integrations that power ingestion, review, and finance workflows across the PMOVES stack. -

- - yt-dlp Status β†’ - + + yt-dlp Status + + } + > +
+ {/* System Stats Bar */} + + + {/* Tier Navigation */} +
+

+ Filter by Category +

+ +
+ + {/* Search Bar */} +
+ setSearchQuery(e.target.value)} + className="w-full px-4 py-3 bg-void-soft border border-border-subtle rounded-lg font-body text-sm text-ink-primary placeholder:text-ink-muted focus:outline-none focus:border-cata-cyan focus:ring-1 focus:ring-cata-cyan" + /> + + {filteredServices.length} of {SERVICE_CATALOG.length} + +
+ + {/* Services Grid */} +
+ {filteredServices.map((service) => { + const color = service.color as string; + const serviceHealth = health[service.slug]; + const status = serviceHealth?.status || 'unknown'; + const href = service.endpoints.length > 0 + ? `/dashboard/services/${service.slug}` + : '#'; + + return ( + + {/* Health indicator */} +
+ +
+ + {/* Header */} +
+ {service.category} +

+ {service.title} +

+
+ + {/* Icon */} +
+ {service.title.charAt(0)} +
+ + {/* Description */} +

+ {service.summary} +

+ + {/* Endpoints */} + {service.endpoints.length > 0 && ( +
+ {service.endpoints.slice(0, 2).map((endpoint) => ( + + {endpoint.name} + + ))} + {service.endpoints.length > 2 && ( + + +{service.endpoints.length - 2} more + + )} +
+ )} + + {/* Footer */} +
+ + {service.slug} + + + {service.endpoints.length > 0 ? 'View details' : 'External link'} + + +
+ + ); + })}
-
-
- {INTEGRATION_SERVICES.map((service) => ( - -
-
Integration
-

{service.title}

-

{service.summary}

+ + {/* No results */} + {filteredServices.length === 0 && ( +
+
+ No services found
- View runbook β†’ - - ))} +

+ Try adjusting your search or filter criteria +

+
+ )} + + {/* External Dashboards */} +
+

External Dashboards

+
+ {PORT_LINKS.map((item) => ( + +
+ :{item.port} +
+ + {item.name} + +
+ ))} +
+
); diff --git a/pmoves/ui/app/page.tsx b/pmoves/ui/app/page.tsx index d431926d08..7825f14f52 100644 --- a/pmoves/ui/app/page.tsx +++ b/pmoves/ui/app/page.tsx @@ -1,19 +1,13 @@ import Link from 'next/link'; +import { SystemHubSection } from '@/components/hub/SystemHubSection'; -type Feature = { - title: string; - description: string; - accent: string; -}; - -type ModuleTile = { - title: string; - blurb: string; - capabilities: string[]; - href: string; -}; +/* ═══════════════════════════════════════════════════════════════════════════ + POWERFULMOVES Landing Page β€” Megaman Γ— Transformers + Cataclysm Studios Inc. + ═══════════════════════════════════════════════════════════════════════════ */ -type PipelineStage = { +type PipelineStep = { + num: string; title: string; summary: string; highlight: string; @@ -26,92 +20,11 @@ type PersonaAvatar = { description: string; }; -type LinkDef = { label: string; href: string; health?: string; optional?: boolean }; - -const features: Feature[] = [ - { - title: 'Cymatic Storyweaving', - description: - 'Visualize resonance: sound-reactive plots show how data pulses across PMOVES, making invisible flows tangible for every collaborator.', - accent: 'var(--cataclysm-cyan)', - }, - { - title: 'Geometry Bus', - description: - 'Align holographic schemas, blueprint automations, and choreograph supply chains with the geometry-first logic core of PMOVES.', - accent: 'var(--cataclysm-gold)', - }, - { - title: 'Chit System', - description: - 'Tokenize commitments, route resources, and surface accountability loops that let communities move with precision and care.', - accent: 'var(--cataclysm-ember)', - }, -]; - -const modules: ModuleTile[] = [ - { - title: 'Agent Zero Β· Conversational Core', - blurb: - 'Natural language entry point that orchestrates Supabase data, creative automations, and infrastructure workflows.', - capabilities: ['Command console', 'Task delegation', 'Observability traces'], - href: '/dashboard/agent-zero', - }, - { - title: 'Archon Β· Knowledge & Personas', - blurb: 'Surface project knowledge, persona prompts, and geometry constellations for guided research.', - capabilities: ['Persona studio', 'Explainability', 'Geometry exports'], - href: '/dashboard/archon', - }, - { - title: 'Creator Pipeline Β· ComfyUI to Publish', - blurb: - 'Ingest renders, audio, and storyboards with the MinIO + Supabase loop documented in the Creator Pipeline runbook.', - capabilities: ['ComfyUI uploads', 'Supabase approvals', 'Discord & Jellyfin publish'], - href: '/dashboard/ingest', - }, - { - title: 'Notebook Workbench Β· Model Ops', - blurb: 'Manage runtime catalogs, seed embeddings, and test inference routes from any device.', - capabilities: ['Model registry', 'Runtime diagnostics', 'GPU / CPU failover'], - href: '/dashboard/notebook', - }, - { - title: 'Finance & Health Β· Automations', - blurb: - 'Monitor Firefly III and Wger data streams translated into CGPs so squads can act on weekly insights.', - capabilities: ['Supabase sync', 'CGP visualizations', 'Cost-aware prompts'], - href: '/dashboard/services', - }, - { - title: 'Monitoring & Observability', - blurb: 'Grafana, Prometheus, and Channel Monitor dashboards ensure distributed services stay aligned.', - capabilities: ['Health badges', 'Latency probes', 'Alert routing'], - href: '/dashboard/monitor', - }, -]; - -const pipeline: PipelineStage[] = [ - { - title: 'Create & Upload', - summary: 'ComfyUI renders assets and pushes them to MinIO with the PMOVES upload nodes.', - highlight: 'GPU-assisted render bundles, uv-managed environments, and tagged filenames keep the flow deterministic.', - }, - { - title: 'Webhook β†’ Supabase', - summary: 'Render Webhook stamps studio_board rows, handing metadata to Supabase for approvals.', - highlight: 'Auto-approve toggles and namespace conventions align assets with geometry constellations.', - }, - { - title: 'Review & Approve', - summary: 'Operators triage submissions in the Studio Board and apply persona-aligned feedback.', - highlight: 'Tags funnel into Indexer facets so creator squads can search, remix, and federate outputs.', - }, - { - title: 'Publish & Broadcast', - summary: 'Publisher emits Discord embeds, refreshes Jellyfin, and mirrors CGPs on the Geometry Bus.', - highlight: 'Audit logs and Chit signals prove where every asset travels across the PMOVES mesh.', - }, +const pipeline: PipelineStep[] = [ + { num: '01', title: 'Create', description: 'ComfyUI renders assets and pushes to MinIO' }, + { num: '02', title: 'Webhook', description: 'Render webhook stamps studio_board rows' }, + { num: '03', title: 'Approve', description: 'Operators triage in Studio Board' }, + { num: '04', title: 'Publish', description: 'Emit to Discord, Jellyfin, Geometry Bus' }, ]; const personas: PersonaAvatar[] = [ @@ -138,248 +51,9 @@ const personas: PersonaAvatar[] = [ }, ]; -function HeroSection() { - return ( -
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - -
-
- -
- - Cataclysm Studios Inc. - -
-

Powerful Moves for everyday creators

-

- From CATACLYSM STUDIOS INC. and the POWERFULMOVES initiative comes a symphony of cymatics, holography, and precision geometry. PMOVES orchestrates the Chit System, Geometry Bus, and cooperative automations so collectives can prototype, publish, and scale together. -

-

- Explore PMOVES.AI for the full capability atlas, tap into cataclsmtudios.com for the studio constellation, and connect via the Cataclysm home lab network spanning cataclsysmstudios.net. -

-
- -
- - Join the creator community - - - Launch engineer console - -
- -
- {features.map((feature) => ( -
-

- {feature.title} -

-

{feature.description}

-
- ))} -
- -

- Cyan Β· Ember Β· Forest Β· Gold β€” the Cataclysm palette guiding every move. -

-
-
- ); -} - -function UnifiedModulesSection() { - return ( -
-
-
- - Unified Portal Β· Modular Reach - -

- Everything in PMOVES is reachable from one surface -

-

- The console blends conversational orchestration, knowledge navigation, creator automations, and operational health in a - responsive layout aligned with the unified UI design story. Choose a module to dive deeper or hand off tasks to Agent Zero. -

-
- -
-
- ); -} - -function CreatorPipelineSection() { - return ( -
-
-
- - Creator Pipeline Β· ComfyUI β†’ Publish - -

Launch the full creative flywheel

-

- The documented Creator Pipeline keeps renders, voices, and geometry aligned. Follow the loop to move assets from ComfyUI rigs to - Supabase approvals and onward to Discord, Jellyfin, and geometry constellations without losing context. -

-
-
    - {pipeline.map((stage, index) => ( -
  1. -
    - {index + 1} -
    -

    {stage.title}

    -

    {stage.summary}

    -

    {stage.highlight}

    -
  2. - ))} -
- -
-
- ); -} - -function PersonaShowcaseSection() { - return ( -
-
-
- - Avatars & Voice Signatures - -

Give every agent a face and a vibe

-

- Personas align with the avatar guidance in the unified UI plan. Style presets keep artwork cohesive across CGPs, chat, and - voice dropsβ€”ready for ComfyUI regeneration or VibeVoice playback at any moment. -

-
-
- {personas.map((persona) => ( -
-
-
- - {persona.role} - -

{persona.name}

-
-
- {persona.name.slice(0, 1)} -
-
-

{persona.description}

-
- Theme Β· {persona.theme} -
-
- ))} -
-

- Swap presets via creator pipelines Β· Keep voices synced with VibeVoice + RVC bundles -

-
-
- ); -} - -async function probe(url?: string) { - if (!url) return undefined; - try { - const res = await fetch(url, { next: { revalidate: 0 } }); - return res.ok; - } catch { - return false; - } -} - -function getDashboardLinks(): LinkDef[] { - const gpuPort = process.env.HIRAG_V2_GPU_HOST_PORT || '8087'; - const agentZeroBase = (process.env.NEXT_PUBLIC_AGENT_ZERO_URL || 'http://localhost:8080').replace(/\/$/, ''); - const archonBase = (process.env.NEXT_PUBLIC_ARCHON_URL || 'http://localhost:8091').replace(/\/$/, ''); - const jellyfinBase = (process.env.NEXT_PUBLIC_JELLYFIN_URL || 'http://localhost:8096').replace(/\/$/, ''); - const supaserchPort = process.env.SUPASERCH_HOST_PORT || process.env.SUPASERCH_PORT || '8099'; - const supaserchBase = (process.env.NEXT_PUBLIC_SUPASERCH_URL || `http://localhost:${supaserchPort}`).replace(/\/$/, ''); +/* ───────────────────────────────────────────────────────────────────────────── + Hero Section β€” Megaman Γ— Transformers Style + ───────────────────────────────────────────────────────────────────────────── */ return [ { label: 'Notebook dashboard', href: '/dashboard/notebook' }, @@ -454,112 +128,233 @@ function getDashboardLinks(): LinkDef[] { ]; } -function OperatorConsole({ - primaryHref, - primaryLabel, - links, - statuses, -}: { - primaryHref: string; - primaryLabel: string; - links: LinkDef[]; - statuses: Array; -}) { +/* ───────────────────────────────────────────────────────────────────────────── + Pipeline Section + ───────────────────────────────────────────────────────────────────────────── */ + +function PipelineSection() { return ( -
-
-

PMOVES Operator Console

-

- Sign in to manage ingestion workflows, upload new assets, and monitor Supabase processing pipelines. -

+
+ {/* Background accent */} +
+
-
- - {primaryLabel} - - - View ingestion dashboard - + +
+ {/* Header */} +
+
+ + [ CREATOR PIPELINE ] + +

+ COMFYUI + + PUBLISH +

+
+
+

+ Launch the full creative flywheel. Renders, voices, and geometry + aligned through the documented pipeline. +

+ + View pipeline + +
+
+ + {/* Pipeline steps */} +
+ {pipeline.map((step) => ( +
+ + {step.num} + +

+ {step.title} +

+

+ {step.description} +

+
+ ))} +
+ + {/* Links row */} +
-
-
- {links.map((link, idx) => { - const health = link.health; - const status = statuses[idx]; - const badge = health - ? status === true - ? ( - - healthy - - ) - : status === false - ? ( - - down - - ) - : ( - - n/a - - ) - : ( - - link - - ); - return ( - + ); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Personas Section + ───────────────────────────────────────────────────────────────────────────── */ + +function PersonasSection() { + return ( +
+
+ {/* Header */} +
+ + [ AGENT PERSONAS ] + +

+ GIVE EVERY AGENT +
+ A FACE & A VIBE +

+
+ + {/* Personas grid */} +
+ {personas.map((persona) => ( +
+ {/* Avatar */} + + + {/* Content */} +
+

+ {persona.name} +

+

+ {persona.role} +

+
+ + {/* Theme tag */} +
+ Theme +

+ {persona.theme} +

+
+
+ ))}
+ + {/* Footer note */} +

+ Swap presets via creator pipelines // Keep voices synced with VibeVoice + RVC +

); } -export default async function HomePage() { - const hasBootJwt = Boolean( - process.env.NEXT_PUBLIC_SUPABASE_BOOT_USER_JWT || process.env.SUPABASE_BOOT_USER_JWT, +/* ───────────────────────────────────────────────────────────────────────────── + Footer + ───────────────────────────────────────────────────────────────────────────── */ + +function Footer() { + return ( +
+
+
+ {/* Brand */} +
+
+
+ POWERFULMOVES +
+

+ Local-first autonomy, reproducible provisioning, and self-improving research loops. +

+

+ Cataclysm Studios Inc. // {new Date().getFullYear()} +

+
+ + {/* Links */} +
+
+

Platform

+
    +
  • Ingestion
  • +
  • Notebook
  • +
  • Services
  • +
+
+
+

Resources

+ +
+
+

Connect

+ +
+
+
+ + {/* Bottom bar */} +
+
+ + + + +
+ The Cataclysm palette guiding every move +
+
+
); - const primaryHref = hasBootJwt ? '/dashboard/ingest' : '/login'; - const primaryLabel = hasBootJwt ? 'Open dashboard' : 'Continue to login'; - const links = getDashboardLinks(); - const statuses = await Promise.all(links.map((link) => probe(link.health))); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Main Page + ───────────────────────────────────────────────────────────────────────────── */ +export default function HomePage() { return ( <> - - - - - + + + +