diff --git a/pmoves/Makefile b/pmoves/Makefile index a1c53a5b7c..cc6f52922b 100644 --- a/pmoves/Makefile +++ b/pmoves/Makefile @@ -1413,11 +1413,26 @@ up-legacy-both: ## Ensure v1 hi-rag gateway CPU+GPU are up # always operate on the same stack regardless of cwd/path. PROJECT ?= pmoves export PROJECT +N8N_SUBMODULE_DIR ?= ../PMOVES-n8n +N8N_CANONICAL_FLOWS_DIR ?= $(N8N_SUBMODULE_DIR)/workflows # n8n persistence mode: -# - sqlite: simplest local bring-up (default) -# - postgres: production-grade n8n DB (recommended for VPS) -N8N_DB ?= sqlite +# - postgres: production-grade n8n DB (default) +# - sqlite: legacy/local escape hatch only +N8N_DB ?= postgres +N8N_DB_NAME ?= n8n +N8N_DB_USER ?= n8n +N8N_DB_PASSWORD ?= pmoves_n8n_local +N8N_DB_SCHEMA ?= public +N8N_BASE_URL ?= http://localhost:5678 +N8N_API_URL ?= $(N8N_BASE_URL)/api/v1 +N8N_OWNER_EMAIL ?= $(if $(PMOVES_OPERATOR_EMAIL),$(PMOVES_OPERATOR_EMAIL),$(SUPABASE_BOOT_USER_EMAIL)) +N8N_OWNER_FIRST_NAME ?= PMOVES +N8N_OWNER_LAST_NAME ?= Operator +N8N_API_KEY_LABEL ?= PMOVES.AI automation bootstrap +N8N_ENV_WRITE_FILE ?= .env.local +export N8N_DB N8N_DB_NAME N8N_DB_USER N8N_DB_PASSWORD N8N_DB_SCHEMA +export N8N_BASE_URL N8N_API_URL N8N_OWNER_EMAIL N8N_OWNER_FIRST_NAME N8N_OWNER_LAST_NAME N8N_API_KEY_LABEL ifeq ($(N8N_DB),postgres) N8N_DB_STACK_FILE := -f docker-compose.n8n.postgres.yml else @@ -2344,8 +2359,13 @@ up-agents-integrations: ## Start agents using your forks (builds from $(INTEGRAT @$(DC) -f docker-compose.agents.integrations.yml --profile agents up -d nats agent-zero archon archon-ui mesh-agent publisher-discord @echo "✔ Agents started from integrations workspace. Workspace: $(INTEGRATIONS_WORKSPACE)" -.PHONY: up-n8n -up-n8n: ensure-env-shared +.PHONY: up-n8n n8n-sync-submodule-flows n8n-api-bootstrap n8n-import-flows n8n-activate-flows n8n-sync-supabase-registry n8n-bootstrap n8n-export-repo-flows +n8n-sync-submodule-flows: ## Mirror canonical PMOVES-n8n workflows into pmoves/n8n/flows + @$(PYTHON) -c "from pathlib import Path; import shutil; src=Path(r'$(N8N_CANONICAL_FLOWS_DIR)'); dst=Path(r'$(CURDIR)/n8n/flows'); dst.mkdir(parents=True, exist_ok=True); [shutil.copy2(path, dst / path.name) for path in sorted(src.glob('*.json'))]" + @echo "✔ Mirrored PMOVES-n8n workflows into pmoves/n8n/flows" + +up-n8n: ensure-env-shared n8n-sync-submodule-flows + @docker network create cataclysm-net >/dev/null 2>&1 || true @if [ "$(N8N_DB)" = "postgres" ]; then \ echo "→ n8n DB mode: postgres"; \ $(N8N_DC) up -d n8n-db n8n n8n-runners; \ @@ -2359,6 +2379,32 @@ down-n8n: ## Stop n8n workflow services @$(N8N_DC) stop n8n n8n-runners n8n-db >/dev/null 2>&1 || true @$(N8N_DC) rm -f n8n n8n-runners n8n-db >/dev/null 2>&1 || true +n8n-api-bootstrap: ## Create/login the n8n owner and mint a fresh Public API key into the local env file + @$(LOAD_ENV_SHARED); $(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/bootstrap_n8n_api.py" \ + --base-url "$(N8N_BASE_URL)" \ + --api-url "$(N8N_API_URL)" \ + --email "$(N8N_OWNER_EMAIL)" \ + --first-name "$(N8N_OWNER_FIRST_NAME)" \ + --last-name "$(N8N_OWNER_LAST_NAME)" \ + --label "$(N8N_API_KEY_LABEL)" \ + --write-env "$(N8N_ENV_WRITE_FILE)" + +n8n-import-flows: ## Import canonical PMOVES-n8n workflows into the live n8n instance + @$(LOAD_ENV_SHARED); $(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/import_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" --skip-activation + +n8n-activate-flows: ## Publish canonical PMOVES-n8n workflows (keeps chat-platform voice flows inactive unless VOICE_PLATFORMS=1) + @$(LOAD_ENV_SHARED); $(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/import_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" --activate-only $(if $(VOICE_PLATFORMS),--voice-platforms,) + +n8n-sync-supabase-registry: ## Upsert live n8n workflow state into pmoves_core.n8n_workflow_registry + @$(LOAD_ENV_SHARED); $(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/sync_supabase_registry.py" --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" + +n8n-bootstrap: up-n8n n8n-api-bootstrap n8n-import-flows n8n-activate-flows n8n-sync-supabase-registry ## Bring up n8n, bootstrap API access, import workflows, publish defaults, and sync Supabase tracking + @echo "✔ n8n bootstrap complete" + +n8n-export-repo-flows: ## Export live n8n workflows into PMOVES-n8n and refresh the compatibility mirror + @$(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/export_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" + @$(MAKE) n8n-sync-submodule-flows + .PHONY: up-comfyui comfyui-smoke up-comfyui: ## Start ComfyUI (docker profile; used by n8n pmoves_comfy_gen flow) @$(DC) --profile creator up -d comfyui diff --git a/pmoves/compose/docker-compose.core.yml b/pmoves/compose/docker-compose.core.yml index 1a2908d74c..2fdbcf5cdc 100644 --- a/pmoves/compose/docker-compose.core.yml +++ b/pmoves/compose/docker-compose.core.yml @@ -3,7 +3,7 @@ version: "3.9" services: n8n: build: - context: .. + context: ../../PMOVES-n8n dockerfile: compose/n8n/Dockerfile image: pmoves/n8n:1.115.3-sqlite restart: unless-stopped @@ -42,8 +42,9 @@ services: - "host.docker.internal:host-gateway" volumes: - n8n-data:/home/node/.n8n + - ../../PMOVES-n8n/workflows:/flows:ro healthcheck: - test: ["CMD", "node", "-e", "fetch('http://localhost:5678/rest/healthz').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))"] + test: ["CMD", "node", "-e", "fetch('http://localhost:5678/healthz').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))"] interval: 10s timeout: 5s retries: 12 diff --git a/pmoves/docker-compose.n8n.postgres.yml b/pmoves/docker-compose.n8n.postgres.yml index 3c29ceaa8b..b8bd1bfba9 100644 --- a/pmoves/docker-compose.n8n.postgres.yml +++ b/pmoves/docker-compose.n8n.postgres.yml @@ -10,7 +10,7 @@ services: - POSTGRES_PASSWORD=${N8N_DB_PASSWORD:?set N8N_DB_PASSWORD} volumes: - n8n-db-data:/var/lib/postgresql/data - networks: [cataclysm, api_tier] + networks: [cataclysm] healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 5s @@ -32,4 +32,3 @@ services: volumes: n8n-db-data: {} - diff --git a/pmoves/docker-compose.n8n.yml b/pmoves/docker-compose.n8n.yml index 15904ac361..f1ef2d2c9a 100644 --- a/pmoves/docker-compose.n8n.yml +++ b/pmoves/docker-compose.n8n.yml @@ -7,9 +7,9 @@ x-env-tier-worker: &env-tier-worker services: n8n: build: - context: . + context: ../PMOVES-n8n dockerfile: compose/n8n/Dockerfile - image: pmoves/n8n:2.1.0-sqlite + image: pmoves/n8n:2.1.0-runtime container_name: pmoves-n8n restart: unless-stopped <<: *env-tier-worker @@ -17,6 +17,7 @@ services: - N8N_PORT=5678 - N8N_PROTOCOL=http - N8N_HOST=localhost + - N8N_SECURE_COOKIE=false - N8N_DEFAULT_TIMEZONE=${TZ:-America/New_York} - WEBHOOK_URL=http://localhost:5678 - GENERIC_TIMEZONE=${TZ:-America/New_York} @@ -45,11 +46,12 @@ services: - N8N_LOG_LEVEL=debug - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true ports: ["5678:5678"] + networks: [cataclysm] extra_hosts: - "host.docker.internal:host-gateway" volumes: - n8n-data:/home/node/.n8n - - ./n8n/flows:/flows:ro + - ../PMOVES-n8n/workflows:/flows:ro n8n-runners: image: n8nio/runners:2.1.0 container_name: pmoves-n8n-runners @@ -61,6 +63,7 @@ services: - N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT=300 depends_on: - n8n + networks: [cataclysm] volumes: - n8n-data:/home/node/.n8n volumes: diff --git a/pmoves/docs/NEXT_STEPS.md b/pmoves/docs/NEXT_STEPS.md index 2b3f086df3..aa86086233 100644 --- a/pmoves/docs/NEXT_STEPS.md +++ b/pmoves/docs/NEXT_STEPS.md @@ -12,6 +12,14 @@ _Last updated: 2026-03-12_ - Added `pmoves/docs/PMOVES.AI PLANS/CREATOR_NETWORK_CONTROL_PLANE.md` to frame YouTube, Discord agents, transcribe-and-fetch, model routing, and Tokenism as one creator-network lane. - Next focus: finish review cleanup, get PMOVES.YT/root checks green, and keep the creator/channel-monitor runbooks aligned with the authoritative runtime instead of the compatibility mirror. +### Latest changes (Mar 12, 2026) — n8n Production Control Plane Refresh +- `PMOVES-n8n` is now the authoritative n8n runtime/workflow lane; root `pmoves` consumes it instead of treating `pmoves/n8n/flows` as canon. +- n8n defaults to the dedicated `n8n-db` Postgres sidecar (`make -C pmoves up-n8n`), with SQLite reduced to a legacy escape hatch only. +- Added `make -C pmoves n8n-api-bootstrap` to automate owner bootstrap + Public API key rotation for n8n 2.1. +- Workflow activation/import now targets the n8n Public API path, replacing the failing CLI publish/unpublish fallback for the production lane. +- Added Supabase tracking schema `pmoves_core.n8n_workflow_registry` plus `make -C pmoves n8n-sync-supabase-registry` so PMOVES can inventory live workflow state. +- Next focus: validate the full bootstrap against Postgres-backed n8n, refresh PMOVES.YT from demo to production against the same automation lane, and decide which BotZ/MCP workflows join the shared canonical catalog. + ### Latest changes (Mar 8, 2026) — CI Runner Migration + RG-3 Automation - **AB-9 mitigation:** Migrated 10 lightweight CI jobs from `[self-hosted, Linux, X64]` to `ubuntu-latest`: - `sql-policy-lint`, `python-tests`, `webhook-smoke`, `yt-dlp-bump`, `deploy-gateway-agent` (validate only) diff --git a/pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md b/pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md index e5171ca5dc..dc67dc4227 100644 --- a/pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md +++ b/pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md @@ -72,6 +72,19 @@ Set `EXTERNAL_NEO4J|MEILI|QDRANT|SUPABASE=true` in `.env.local` to skip local in - `make up-nats` - Starts the NATS broker (`agents` profile) and rewrites `.env.local` so `YT_NATS_ENABLE=true` with `NATS_URL=nats://nats:pmoves@nats:4222`. - Use this before opting into the agents profile (Agent Zero, Archon, mesh-agent, Discord publisher). +- `make up-n8n` + - Starts the production/default n8n stack: `n8n`, `n8n-runners`, and the dedicated `n8n-db` Postgres sidecar. + - Canonical workflows come from `PMOVES-n8n/workflows`; `pmoves/n8n/flows` is only a compatibility mirror. +- `make n8n-api-bootstrap` + - Creates or logs into the n8n owner account, rotates the bootstrap Public API key, validates it, and writes `N8N_API_KEY` plus owner credentials into `pmoves/.env.local`. +- `make n8n-import-flows` + - Upserts the canonical `PMOVES-n8n` workflow catalog into the live n8n instance. +- `make n8n-activate-flows` + - Activates the default workflow set through the n8n Public API; keeps chat-platform voice flows inactive unless `VOICE_PLATFORMS=1`. +- `make n8n-sync-supabase-registry` + - Mirrors live workflow state into `pmoves_core.n8n_workflow_registry` so PMOVES can track n8n activity in Supabase. +- `make n8n-bootstrap` + - Full production bootstrap: `up-n8n -> n8n-api-bootstrap -> n8n-import-flows -> n8n-activate-flows -> n8n-sync-supabase-registry`. - `make mindmap-notebook-sync` - Runs `python pmoves/scripts/mindmap_to_notebook.py` to pull `/mindmap/{constellation_id}` entries out of `hi-rag-gateway-v2` and mirror them into Open Notebook via `/api/sources/json`. Requires `MINDMAP_BASE`, `MINDMAP_CONSTELLATION_ID`, `MINDMAP_NOTEBOOK_ID`, and `OPEN_NOTEBOOK_API_TOKEN`. - `make hirag-notebook-sync` diff --git a/pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md b/pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md index 79a8e018c1..83bb80a538 100644 --- a/pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md +++ b/pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md @@ -1,8 +1,8 @@ # n8n Setup Checklist (Supabase → Agent Zero → Discord) -_Last updated: 2025-12-14_ +_Last updated: 2026-03-12_ ## Overview -This guide streamlines importing and running the PMOVES approval and publish workflows in n8n. It targets Supabase CLI on the host, Agent Zero + NATS in Docker, and Discord webhooks. +This guide covers the production n8n path for PMOVES.AI. `PMOVES-n8n` is the authoritative runtime/workflow fork, n8n internals live on the dedicated `n8n-db` Postgres sidecar, and Supabase tracks PMOVES workflow state in `pmoves_core.n8n_workflow_registry`. ## Preflight (quick) - Start stacks: `make up && make up-agents && make up-n8n` @@ -14,8 +14,8 @@ This guide streamlines importing and running the PMOVES approval and publish wor - Supabase CLI running locally: `supabase start` or `make supa-start` - PMOVES stack up: `make up && make up-agents` - n8n running: - - Local/dev (SQLite): `make up-n8n` (UI at `http://localhost:5678`, launches `n8n` + `n8n-runners`) - - VPS/prod (Postgres): `N8N_DB=postgres make up-n8n` (adds `n8n-db` Postgres for durable state) + - Production/default: `make -C pmoves up-n8n` (starts `n8n`, `n8n-runners`, and `n8n-db`) + - Legacy escape hatch only: `N8N_DB=sqlite make -C pmoves up-n8n` - Secrets at hand: `SUPABASE_SERVICE_ROLE_KEY`, `DISCORD_WEBHOOK_URL`, `N8N_RUNNERS_AUTH_TOKEN` ## Environment (n8n) @@ -28,40 +28,50 @@ Set these in n8n (Settings → Variables) or via container env: - `DISCORD_WEBHOOK_USERNAME` = `PMOVES Publisher` - `N8N_RUNNERS_AUTH_TOKEN` = `` - `N8N_DEFAULT_TIMEZONE` = `America/New_York` (aligns cron schedules with project TZ) +- `N8N_DB_NAME` / `N8N_DB_USER` / `N8N_DB_PASSWORD` = dedicated `n8n-db` credentials +- `N8N_OWNER_EMAIL` / `N8N_OWNER_PASSWORD` = owner bootstrap credentials used by `make -C pmoves n8n-api-bootstrap` +- `N8N_API_KEY` = Public API key minted by `make -C pmoves n8n-api-bootstrap` ### n8n persistence mode (SQLite vs Postgres) -By default, `make up-n8n` runs n8n with SQLite for quick local bring-up. +By default, `make -C pmoves up-n8n` runs n8n on the dedicated Postgres sidecar. -For VPS/production, run n8n on Postgres: -- Set `N8N_DB=postgres` -- Set `N8N_DB_NAME`, `N8N_DB_USER`, `N8N_DB_PASSWORD` (in `pmoves/env.shared` or injected secrets) -- Bring up: `N8N_DB=postgres make -C pmoves up-n8n` +For production, keep n8n on Postgres: +- `N8N_DB=postgres` +- `N8N_DB_NAME`, `N8N_DB_USER`, `N8N_DB_PASSWORD` +- `make -C pmoves up-n8n` This uses `pmoves/docker-compose.n8n.postgres.yml` to add a dedicated `n8n-db` container. It does **not** move your PMOVES app data into n8n — it only stores n8n’s own workflow/execution state in Postgres. +Supabase remains the PMOVES system of record for workflow tracking, approvals, publishing, and operator UI state. + > **Supabase runtime note:** The CLI runtime binds REST on port `65421` per `supabase/config.toml`. If you switch back to the docker-compose PostgREST service, update `SUPABASE_REST_URL` accordingly (typically `http://host.docker.internal:54321/rest/v1`). Tip: These defaults are prewired in `docker-compose.n8n.yml`. If you use `make up-n8n`, populate `SUPABASE_SERVICE_ROLE_KEY`, `DISCORD_WEBHOOK_URL`, and `N8N_RUNNERS_AUTH_TOKEN` in `pmoves/env.shared` or `pmoves/.env.local`. The runner token is loaded via `env_file` (not compose-time interpolation) so recreating containers won’t accidentally desync the broker and sidecar. Note: n8n 1.115.3 already executes cron triggers in the main process. Avoid re-adding the deprecated `EXECUTIONS_PROCESS` flag—the service emits a warning and ignores it. +## Production Bootstrap +- `make -C pmoves up-n8n` +- `make -C pmoves n8n-api-bootstrap` +- `make -C pmoves n8n-import-flows` +- `make -C pmoves n8n-activate-flows` +- `make -C pmoves n8n-sync-supabase-registry` +- `make -C pmoves n8n-bootstrap` + +`n8n-api-bootstrap` creates or logs into the n8n owner account, rotates the `PMOVES.AI automation bootstrap` Public API key, validates it against `/api/v1/workflows`, and writes the resulting `N8N_API_KEY` plus owner credentials into `pmoves/.env.local`. + ## Container Tooling - The custom image defined in `compose/n8n/Dockerfile` bakes in the `sqlite3` CLI so DB inspections persist across restarts. - Run `make up-n8n` after pulling updates to rebuild the service when the Dockerfile changes. ## Import Workflows -1. Open n8n → Workflows → Import from File. -2. Import the core approvals stack: - - `pmoves/n8n/flows/approval_poller.json` - - `pmoves/n8n/flows/echo_publisher.json` -3. Import the creative webhooks (requires ComfyUI hosts prepared via [`pmoves/creator/README.md`](../creator/README.md)): - - `pmoves/n8n/flows/wan_to_cgp.webhook.json` - - `pmoves/n8n/flows/qwen_to_cgp.webhook.json` - - `pmoves/n8n/flows/vibevoice_to_cgp.webhook.json` -4. (Optional) Import the audio enrichment flows: - - `pmoves/n8n/flows/vibevoice_audio_ingest.json` - - `pmoves/n8n/flows/vibevoice_discord_preview.json` -5. Keep everything inactive until env is confirmed (Supabase keys, MinIO buckets, Discord webhooks). +Canonical workflow JSON now lives in `PMOVES-n8n/workflows/`. + +- Recommended: `make -C pmoves n8n-import-flows` +- Activation: `make -C pmoves n8n-activate-flows` +- Full bootstrap: `make -C pmoves n8n-bootstrap` + +The root `pmoves/n8n/flows/` directory is now only a compatibility mirror. ## Validate Env Bindings - Approval Poller @@ -85,6 +95,8 @@ Note: n8n 1.115.3 already executes cron triggers in the main process. Avoid re-a 5. Activate echo publisher → confirm Discord embed (title/link/thumbnail if provided) 6. Optional: Post directly to n8n webhook (flow must be active) - `make n8n-webhook-demo` +7. Sync the live workflow registry back into Supabase: + - `make -C pmoves n8n-sync-supabase-registry` ### Voice platform flows (Discord/Telegram) The repo includes optional chat-platform voice agent flows (Discord/Telegram) that require additional credentials and/or custom n8n nodes. @@ -126,6 +138,8 @@ These flows extend the core approval automations so we can surface RVC voice out ## Troubleshooting - 404 from Supabase in n8n: ensure `/rest/v1` is included in `SUPABASE_REST_URL`. +- `401` from `/api/v1/workflows`: run `make -C pmoves n8n-api-bootstrap` to rotate the Public API key. +- Workflow import works but activation fails: use the Public API path (`n8n-api-bootstrap` + `n8n-activate-flows`) instead of the legacy CLI publish fallback. - 503 from Agent Zero: confirm NATS + Agent Zero are running (`make up-agents`). - Discord no messages: verify `DISCORD_WEBHOOK_URL` and check rate limits in n8n logs. - n8n cannot reach host services on Linux: replace `host.docker.internal` with the host IP or Docker gateway (`172.17.0.1`). diff --git a/pmoves/docs/PMOVES.AI PLANS/ROADMAP.md b/pmoves/docs/PMOVES.AI PLANS/ROADMAP.md index 38e15d5cd3..6b2f426cbd 100644 --- a/pmoves/docs/PMOVES.AI PLANS/ROADMAP.md +++ b/pmoves/docs/PMOVES.AI PLANS/ROADMAP.md @@ -12,6 +12,12 @@ A production-ready, self-hostable orchestration mesh for creative + agent worklo - the Supabase docs sync contract was refreshed for the current CLI stack: `pmoves_core.tool_docs` writes now use schema-profile headers plus URL-encoded `on_conflict`. - root `pmoves/services/pmoves-yt` remains as a compatibility shim so existing tests/import paths keep working while production moves to the submodule. - downloader defaults are being normalized around the authoritative runtime: PMOVES.YT now documents the modern client/token path, root compose passes explicit companion wiring, and Jellyfin/channel-monitor docs are being moved off older MCP and future-work framing. +- March 12 n8n production-path remediation landed locally: + - `PMOVES-n8n` is now the authoritative runtime/workflow lane consumed by the root repo. + - `make -C pmoves up-n8n` now defaults to the dedicated `n8n-db` Postgres sidecar instead of SQLite. + - n8n owner/bootstrap automation is scripted (`n8n-api-bootstrap`) so Public API keys no longer depend on manual UI steps. + - workflow activation now targets the n8n 2.1 Public API (`/api/v1/workflows/{id}/activate|deactivate`) instead of the brittle CLI publish fallback. + - Supabase tracking contract added: `pmoves_core.n8n_workflow_registry` stores live workflow state synced from n8n. - March 7 merge wave completed on `main`: `#814`, `#815`, `#816`, `#817`, `#818`, `#819`, `#820`, `#821` (8 PRs, 3 batches). - Chrome extension security hardening landed in `#821`: 9 CodeRabbit review items addressed (auth storage isolation, XSS remediation, mock server hardening, timeout guards, state management fixes, CSP). - Distributed topology documentation + examples landed in `#820`. diff --git a/pmoves/env.shared.example b/pmoves/env.shared.example index 7a1f12f61c..e4ccab796d 100644 --- a/pmoves/env.shared.example +++ b/pmoves/env.shared.example @@ -100,6 +100,21 @@ NEXT_PUBLIC_SUPABASE_BOOT_USER_JWT= AUTH_BOOTSTRAP_MODE=jwt AUTH_BOOTSTRAP_STRICT=0 +# n8n production defaults +# Keep n8n internals on the dedicated sidecar Postgres. PMOVES domain state still lives in Supabase. +N8N_DB=postgres +N8N_DB_NAME=n8n +N8N_DB_USER=n8n +N8N_DB_PASSWORD=CHANGE_ME_N8N_DB_PASSWORD +N8N_DB_SCHEMA=public +N8N_BASE_URL=http://localhost:5678 +N8N_API_URL=${N8N_BASE_URL}/api/v1 +N8N_OWNER_EMAIL=${SUPABASE_BOOT_USER_EMAIL} +N8N_OWNER_PASSWORD= +N8N_OWNER_FIRST_NAME=PMOVES +N8N_OWNER_LAST_NAME=Operator +N8N_API_KEY= + # Optional Google OAuth (Supabase Auth) SUPABASE_AUTH_EXTERNAL_GOOGLE_ENABLED=false SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID= diff --git a/pmoves/n8n/README.md b/pmoves/n8n/README.md new file mode 100644 index 0000000000..6173eb3431 --- /dev/null +++ b/pmoves/n8n/README.md @@ -0,0 +1,15 @@ +# PMOVES n8n Mirror + +`pmoves/n8n/flows/` is now a compatibility mirror. + +Canonical n8n workflow ownership lives in [`PMOVES-n8n/workflows/`](../../PMOVES-n8n/workflows). The parent repo keeps this mirror so older docs, UI links, and operator muscle memory do not break during the transition. + +Canonical edit path: + +```bash +make -C pmoves n8n-api-bootstrap +make -C pmoves n8n-sync-submodule-flows +make -C pmoves n8n-import-flows +make -C pmoves n8n-activate-flows +make -C pmoves n8n-sync-supabase-registry +``` diff --git a/pmoves/n8n/flows/discord_voice_agent.json b/pmoves/n8n/flows/discord_voice_agent.json index 1bacc4b3e7..3cafbef84f 100644 --- a/pmoves/n8n/flows/discord_voice_agent.json +++ b/pmoves/n8n/flows/discord_voice_agent.json @@ -23,10 +23,10 @@ } }, { - "parameters": { - "mode": "rules", - "rules": { - "values": [ + "parameters": { + "mode": "rules", + "rules": { + "values": [ { "conditions": { "options": { @@ -417,19 +417,6 @@ "versionId": "e4e60c53-cc9f-4903-9f62-d8c9e3f65e09", "versionCounter": 4, "triggerCount": 0, - "tags": [ - { - "updatedAt": "2025-12-12T13:18:03.530Z", - "createdAt": "2025-12-12T13:18:03.530Z", - "id": "0A3B8qR44ylHUdFz", - "name": "voice-agent" - }, - { - "updatedAt": "2025-12-12T13:18:05.345Z", - "createdAt": "2025-12-12T13:18:05.345Z", - "id": "LR1zVxH2cj8fd7cd", - "name": "discord" - } - ] + "tags": [] } ] diff --git a/pmoves/n8n/flows/github_runner_autoscaler.json b/pmoves/n8n/flows/github_runner_autoscaler.json index 86c4870850..b604dc8c30 100644 --- a/pmoves/n8n/flows/github_runner_autoscaler.json +++ b/pmoves/n8n/flows/github_runner_autoscaler.json @@ -17,7 +17,10 @@ "name": "Schedule Trigger", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.1, - "position": [480, 240] + "position": [ + 480, + 240 + ] }, { "parameters": { @@ -29,7 +32,10 @@ "name": "Get Runner Status", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [700, 240] + "position": [ + 700, + 240 + ] }, { "parameters": { @@ -39,7 +45,10 @@ "name": "Analyze Runner Status", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [920, 240] + "position": [ + 920, + 240 + ] }, { "parameters": { @@ -62,7 +71,10 @@ "name": "Check Scale Needed", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1140, 240] + "position": [ + 1140, + 240 + ] }, { "parameters": { @@ -72,7 +84,10 @@ "name": "Check Cloud Capacity", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1380, 120] + "position": [ + 1380, + 120 + ] }, { "parameters": { @@ -89,7 +104,10 @@ "name": "Can Scale Up?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1600, 120] + "position": [ + 1600, + 120 + ] }, { "parameters": { @@ -99,7 +117,10 @@ "name": "Prepare Scale Alert", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1820, 0] + "position": [ + 1820, + 0 + ] }, { "parameters": { @@ -115,7 +136,10 @@ "name": "Send Discord Alert", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [2040, 0] + "position": [ + 2040, + 0 + ] }, { "parameters": { @@ -125,7 +149,10 @@ "name": "Prepare Normal Status", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1380, 380] + "position": [ + 1380, + 380 + ] }, { "parameters": { @@ -141,7 +168,10 @@ "name": "Send Status Update", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [1600, 380] + "position": [ + 1600, + 380 + ] }, { "parameters": { @@ -151,7 +181,10 @@ "name": "Prepare Blocked Alert", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1820, 240] + "position": [ + 1820, + 240 + ] }, { "parameters": { @@ -167,42 +200,125 @@ "name": "Send Blocked Alert", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [2040, 240] + "position": [ + 2040, + 240 + ] } ], "connections": { "Schedule Trigger": { - "main": [[{"node": "Get Runner Status", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Get Runner Status", + "type": "main", + "index": 0 + } + ] + ] }, "Get Runner Status": { - "main": [[{"node": "Analyze Runner Status", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Analyze Runner Status", + "type": "main", + "index": 0 + } + ] + ] }, "Analyze Runner Status": { - "main": [[{"node": "Check Scale Needed", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Check Scale Needed", + "type": "main", + "index": 0 + } + ] + ] }, "Check Scale Needed": { "main": [ - [{"node": "Check Cloud Capacity", "type": "main", "index": 0}], - [{"node": "Prepare Normal Status", "type": "main", "index": 0}] + [ + { + "node": "Check Cloud Capacity", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepare Normal Status", + "type": "main", + "index": 0 + } + ] ] }, "Check Cloud Capacity": { - "main": [[{"node": "Can Scale Up?", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Can Scale Up?", + "type": "main", + "index": 0 + } + ] + ] }, "Can Scale Up?": { "main": [ - [{"node": "Prepare Scale Alert", "type": "main", "index": 0}], - [{"node": "Prepare Blocked Alert", "type": "main", "index": 0}] + [ + { + "node": "Prepare Scale Alert", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepare Blocked Alert", + "type": "main", + "index": 0 + } + ] ] }, "Prepare Scale Alert": { - "main": [[{"node": "Send Discord Alert", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Send Discord Alert", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Normal Status": { - "main": [[{"node": "Send Status Update", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Send Status Update", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Blocked Alert": { - "main": [[{"node": "Send Blocked Alert", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Send Blocked Alert", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": { @@ -213,6 +329,10 @@ "meta": { "instanceId": "pmoves-github" }, - "tags": ["github", "runners", "autoscaling", "ci-cd"] + "pinData": null, + "versionId": "3446e348-3748-4c23-a713-31d798c7cc95", + "versionCounter": 1, + "triggerCount": 0, + "tags": [] } ] diff --git a/pmoves/n8n/flows/github_webhook_processor.json b/pmoves/n8n/flows/github_webhook_processor.json index 782923a42c..c88250fbc2 100644 --- a/pmoves/n8n/flows/github_webhook_processor.json +++ b/pmoves/n8n/flows/github_webhook_processor.json @@ -14,7 +14,10 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2, "webhookId": "github-runner-webhook", - "position": [440, 280] + "position": [ + 440, + 280 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Parse Webhook Headers", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [660, 280] + "position": [ + 660, + 280 + ] }, { "parameters": { @@ -53,7 +59,10 @@ "name": "Check Event Type", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [880, 280] + "position": [ + 880, + 280 + ] }, { "parameters": { @@ -63,7 +72,10 @@ "name": "Create NATS Event Envelope", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1100, 120] + "position": [ + 1100, + 120 + ] }, { "parameters": { @@ -75,8 +87,14 @@ "options": { "headerParametersUi": { "parameter": [ - {"name": "subject", "value": "={{ $json.nats_subject }}"}, - {"name": "content-type", "value": "application/json"} + { + "name": "subject", + "value": "={{ $json.nats_subject }}" + }, + { + "name": "content-type", + "value": "application/json" + } ] } } @@ -85,7 +103,10 @@ "name": "Publish to NATS", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [1320, 120] + "position": [ + 1320, + 120 + ] }, { "parameters": { @@ -95,7 +116,10 @@ "name": "Prepare Discord Notification", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1540, 120] + "position": [ + 1540, + 120 + ] }, { "parameters": { @@ -111,7 +135,10 @@ "name": "Send Discord Notification", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [1760, 120] + "position": [ + 1760, + 120 + ] }, { "parameters": { @@ -122,7 +149,10 @@ "name": "Respond Success", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, - "position": [1980, 120] + "position": [ + 1980, + 120 + ] }, { "parameters": { @@ -132,7 +162,10 @@ "name": "Log Ignored Event", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1100, 380] + "position": [ + 1100, + 380 + ] }, { "parameters": { @@ -143,36 +176,107 @@ "name": "Respond Ignored", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, - "position": [1320, 380] + "position": [ + 1320, + 380 + ] } ], "connections": { "Webhook: GitHub Runner Events": { - "main": [[{"node": "Parse Webhook Headers", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Parse Webhook Headers", + "type": "main", + "index": 0 + } + ] + ] }, "Parse Webhook Headers": { - "main": [[{"node": "Check Event Type", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Check Event Type", + "type": "main", + "index": 0 + } + ] + ] }, "Check Event Type": { "main": [ - [{"node": "Create NATS Event Envelope", "type": "main", "index": 0}], - [{"node": "Log Ignored Event", "type": "main", "index": 0}] + [ + { + "node": "Create NATS Event Envelope", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Ignored Event", + "type": "main", + "index": 0 + } + ] ] }, "Create NATS Event Envelope": { - "main": [[{"node": "Publish to NATS", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish to NATS", + "type": "main", + "index": 0 + } + ] + ] }, "Publish to NATS": { - "main": [[{"node": "Prepare Discord Notification", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Discord Notification", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Discord Notification": { - "main": [[{"node": "Send Discord Notification", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Send Discord Notification", + "type": "main", + "index": 0 + } + ] + ] }, "Send Discord Notification": { - "main": [[{"node": "Respond Success", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Respond Success", + "type": "main", + "index": 0 + } + ] + ] }, "Log Ignored Event": { - "main": [[{"node": "Respond Ignored", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Respond Ignored", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": { @@ -183,6 +287,10 @@ "meta": { "instanceId": "pmoves-github" }, - "tags": ["github", "webhooks", "nats", "ci-cd"] + "pinData": null, + "versionId": "58a9f471-9d35-4f01-beb6-2049c48bbfe5", + "versionCounter": 1, + "triggerCount": 0, + "tags": [] } ] diff --git a/pmoves/n8n/flows/langextract_orchestrator.json b/pmoves/n8n/flows/langextract_orchestrator.json index ace5614ddf..8dd93ff7c0 100644 --- a/pmoves/n8n/flows/langextract_orchestrator.json +++ b/pmoves/n8n/flows/langextract_orchestrator.json @@ -252,9 +252,9 @@ "description": "Process documents via LangExtract orchestrator pipeline. Accepts file paths and returns extraction results with chunk counts and ingestion status." }, "pinData": null, - "versionId": "langextract-orch-v1", + "versionId": "c904e951-20ec-4034-acd6-9e2833c4eac9", "versionCounter": 1, "triggerCount": 0, - "tags": ["langextract", "orchestrator", "document-processing"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_audio_analysis.json b/pmoves/n8n/flows/pmoves_audio_analysis.json index 6941cf3f45..fa822c1429 100644 --- a/pmoves/n8n/flows/pmoves_audio_analysis.json +++ b/pmoves/n8n/flows/pmoves_audio_analysis.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Audio Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "FFmpeg-Whisper Transcribe", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Media-Audio Analysis", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -68,7 +80,10 @@ "name": "Merge Results", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 300] + "position": [ + 980, + 300 + ] }, { "parameters": { @@ -78,7 +93,10 @@ "name": "Build CGP Envelope", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1240, 300] + "position": [ + 1240, + 300 + ] }, { "parameters": { @@ -95,7 +113,10 @@ "name": "Index to Hi-RAG", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 300] + "position": [ + 1500, + 300 + ] }, { "parameters": { @@ -112,7 +133,10 @@ "name": "Publish NATS Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 500] + "position": [ + 1500, + 500 + ] }, { "parameters": { @@ -129,7 +153,10 @@ "name": "Auto Research?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1760, 300] + "position": [ + 1760, + 300 + ] }, { "parameters": { @@ -146,42 +173,103 @@ "name": "Trigger DeepResearch", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2020, 200] + "position": [ + 2020, + 200 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Audio Request", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Audio Request", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Audio Request": { "main": [ [ - {"node": "FFmpeg-Whisper Transcribe", "type": "main", "index": 0}, - {"node": "Media-Audio Analysis", "type": "main", "index": 0} + { + "node": "FFmpeg-Whisper Transcribe", + "type": "main", + "index": 0 + }, + { + "node": "Media-Audio Analysis", + "type": "main", + "index": 0 + } ] ] }, "FFmpeg-Whisper Transcribe": { - "main": [[{"node": "Merge Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge Results", + "type": "main", + "index": 0 + } + ] + ] }, "Media-Audio Analysis": { - "main": [[{"node": "Merge Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge Results", + "type": "main", + "index": 0 + } + ] + ] }, "Merge Results": { - "main": [[{"node": "Build CGP Envelope", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Build CGP Envelope", + "type": "main", + "index": 0 + } + ] + ] }, "Build CGP Envelope": { "main": [ [ - {"node": "Index to Hi-RAG", "type": "main", "index": 0}, - {"node": "Publish NATS Event", "type": "main", "index": 0}, - {"node": "Auto Research?", "type": "main", "index": 0} + { + "node": "Index to Hi-RAG", + "type": "main", + "index": 0 + }, + { + "node": "Publish NATS Event", + "type": "main", + "index": 0 + }, + { + "node": "Auto Research?", + "type": "main", + "index": 0 + } ] ] }, "Auto Research?": { "main": [ - [{"node": "Trigger DeepResearch", "type": "main", "index": 0}], + [ + { + "node": "Trigger DeepResearch", + "type": "main", + "index": 0 + } + ], [] ] } @@ -193,9 +281,9 @@ "description": "Audio analysis pipeline: transcription (FFmpeg-Whisper) + emotion/feature analysis (Media-Audio) + Hi-RAG indexing" }, "pinData": null, - "versionId": "audio-analysis-v1", + "versionId": "4e367856-aed6-45ba-abc6-278685ac3bbb", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "audio", "analysis", "transcription"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_channel_monitor.json b/pmoves/n8n/flows/pmoves_channel_monitor.json index b5b6f6ed02..2fd49ea1b3 100644 --- a/pmoves/n8n/flows/pmoves_channel_monitor.json +++ b/pmoves/n8n/flows/pmoves_channel_monitor.json @@ -14,7 +14,10 @@ "name": "Channel Monitor Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Parse Channel Notification", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Publish Channel Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Auto Ingest?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -75,7 +87,10 @@ "name": "Trigger PMOVES.YT Ingest", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 300] + "position": [ + 980, + 300 + ] }, { "parameters": { @@ -85,7 +100,10 @@ "name": "Track Ingest Job", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1240, 300] + "position": [ + 1240, + 300 + ] }, { "parameters": { @@ -95,7 +113,10 @@ "name": "Build Social Preview", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1500, 300] + "position": [ + 1500, + 300 + ] }, { "parameters": { @@ -112,7 +133,10 @@ "name": "Post Preview to Discord", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1760, 200] + "position": [ + 1760, + 200 + ] }, { "parameters": { @@ -129,7 +153,10 @@ "name": "Auto Research?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1760, 400] + "position": [ + 1760, + 400 + ] }, { "parameters": { @@ -146,40 +173,99 @@ "name": "Trigger DeepResearch", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2020, 400] + "position": [ + 2020, + 400 + ] } ], "connections": { "Channel Monitor Webhook": { - "main": [[{"node": "Parse Channel Notification", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Parse Channel Notification", + "type": "main", + "index": 0 + } + ] + ] }, "Parse Channel Notification": { - "main": [[ - {"node": "Publish Channel Event", "type": "main", "index": 0}, - {"node": "Auto Ingest?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Publish Channel Event", + "type": "main", + "index": 0 + }, + { + "node": "Auto Ingest?", + "type": "main", + "index": 0 + } + ] + ] }, "Auto Ingest?": { "main": [ - [{"node": "Trigger PMOVES.YT Ingest", "type": "main", "index": 0}], + [ + { + "node": "Trigger PMOVES.YT Ingest", + "type": "main", + "index": 0 + } + ], [] ] }, "Trigger PMOVES.YT Ingest": { - "main": [[{"node": "Track Ingest Job", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Track Ingest Job", + "type": "main", + "index": 0 + } + ] + ] }, "Track Ingest Job": { - "main": [[{"node": "Build Social Preview", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Build Social Preview", + "type": "main", + "index": 0 + } + ] + ] }, "Build Social Preview": { - "main": [[ - {"node": "Post Preview to Discord", "type": "main", "index": 0}, - {"node": "Auto Research?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Post Preview to Discord", + "type": "main", + "index": 0 + }, + { + "node": "Auto Research?", + "type": "main", + "index": 0 + } + ] + ] }, "Auto Research?": { "main": [ - [{"node": "Trigger DeepResearch", "type": "main", "index": 0}], + [ + { + "node": "Trigger DeepResearch", + "type": "main", + "index": 0 + } + ], [] ] } @@ -191,9 +277,9 @@ "description": "YouTube Channel Monitor integration: receives new content alerts, triggers PMOVES.YT ingestion, posts social preview" }, "pinData": null, - "versionId": "channel-monitor-v1", + "versionId": "1aa17c99-8ea9-48d7-96ea-9e2094d45a73", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "youtube", "channel-monitor", "ingestion"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_comfy_hub.json b/pmoves/n8n/flows/pmoves_comfy_hub.json index 66d47fc941..fff4dff128 100644 --- a/pmoves/n8n/flows/pmoves_comfy_hub.json +++ b/pmoves/n8n/flows/pmoves_comfy_hub.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Generation Job", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Is Qwen Edit?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 100] + "position": [ + 720, + 100 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Is WAN Animate?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 300] + "position": [ + 720, + 300 + ] }, { "parameters": { @@ -76,7 +88,10 @@ "name": "Is Voice?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 500] + "position": [ + 720, + 500 + ] }, { "parameters": { @@ -86,7 +101,10 @@ "name": "Build Qwen Prompt", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 100] + "position": [ + 980, + 100 + ] }, { "parameters": { @@ -96,7 +114,10 @@ "name": "Build WAN Prompt", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 300] + "position": [ + 980, + 300 + ] }, { "parameters": { @@ -106,7 +127,10 @@ "name": "Build Voice Prompt", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 500] + "position": [ + 980, + 500 + ] }, { "parameters": { @@ -123,7 +147,10 @@ "name": "Submit to ComfyUI", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1240, 300] + "position": [ + 1240, + 300 + ] }, { "parameters": { @@ -133,7 +160,10 @@ "name": "Track Job Status", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1500, 300] + "position": [ + 1500, + 300 + ] }, { "parameters": { @@ -150,52 +180,135 @@ "name": "Publish Generation Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1760, 300] + "position": [ + 1760, + 300 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Generation Job", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Generation Job", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Generation Job": { - "main": [[ - {"node": "Is Qwen Edit?", "type": "main", "index": 0}, - {"node": "Is WAN Animate?", "type": "main", "index": 0}, - {"node": "Is Voice?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Is Qwen Edit?", + "type": "main", + "index": 0 + }, + { + "node": "Is WAN Animate?", + "type": "main", + "index": 0 + }, + { + "node": "Is Voice?", + "type": "main", + "index": 0 + } + ] + ] }, "Is Qwen Edit?": { "main": [ - [{"node": "Build Qwen Prompt", "type": "main", "index": 0}], + [ + { + "node": "Build Qwen Prompt", + "type": "main", + "index": 0 + } + ], [] ] }, "Is WAN Animate?": { "main": [ - [{"node": "Build WAN Prompt", "type": "main", "index": 0}], + [ + { + "node": "Build WAN Prompt", + "type": "main", + "index": 0 + } + ], [] ] }, "Is Voice?": { "main": [ - [{"node": "Build Voice Prompt", "type": "main", "index": 0}], + [ + { + "node": "Build Voice Prompt", + "type": "main", + "index": 0 + } + ], [] ] }, "Build Qwen Prompt": { - "main": [[{"node": "Submit to ComfyUI", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Submit to ComfyUI", + "type": "main", + "index": 0 + } + ] + ] }, "Build WAN Prompt": { - "main": [[{"node": "Submit to ComfyUI", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Submit to ComfyUI", + "type": "main", + "index": 0 + } + ] + ] }, "Build Voice Prompt": { - "main": [[{"node": "Submit to ComfyUI", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Submit to ComfyUI", + "type": "main", + "index": 0 + } + ] + ] }, "Submit to ComfyUI": { - "main": [[{"node": "Track Job Status", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Track Job Status", + "type": "main", + "index": 0 + } + ] + ] }, "Track Job Status": { - "main": [[{"node": "Publish Generation Event", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish Generation Event", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {}, @@ -205,9 +318,9 @@ "description": "ComfyUI generation hub: routes generation requests to appropriate workflow (Qwen Image Edit, WAN Animate, VibeVoice)" }, "pinData": null, - "versionId": "comfy-hub-v1", + "versionId": "2fe9ede5-8936-4fbb-a4d2-d9cb291a9729", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "comfyui", "generation", "creative"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_deepresearch_orchestrator.json b/pmoves/n8n/flows/pmoves_deepresearch_orchestrator.json index 1beca2f315..60541bf241 100644 --- a/pmoves/n8n/flows/pmoves_deepresearch_orchestrator.json +++ b/pmoves/n8n/flows/pmoves_deepresearch_orchestrator.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Research Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Publish Research Request", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 300] + "position": [ + 720, + 300 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Call DeepResearch API", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 500] + "position": [ + 720, + 500 + ] }, { "parameters": { @@ -68,7 +80,10 @@ "name": "Process Research Result", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 500] + "position": [ + 980, + 500 + ] }, { "parameters": { @@ -78,7 +93,10 @@ "name": "Build CGP Envelope", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1240, 500] + "position": [ + 1240, + 500 + ] }, { "parameters": { @@ -95,7 +113,10 @@ "name": "Index to Hi-RAG", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 400] + "position": [ + 1500, + 400 + ] }, { "parameters": { @@ -112,7 +133,10 @@ "name": "Publish Result Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 600] + "position": [ + 1500, + 600 + ] }, { "parameters": { @@ -129,7 +153,10 @@ "name": "Auto Publish?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1760, 500] + "position": [ + 1760, + 500 + ] }, { "parameters": { @@ -146,39 +173,92 @@ "name": "Trigger Content Generation", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2020, 400] + "position": [ + 2020, + 400 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Research Request", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Research Request", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Research Request": { "main": [ [ - {"node": "Publish Research Request", "type": "main", "index": 0}, - {"node": "Call DeepResearch API", "type": "main", "index": 0} + { + "node": "Publish Research Request", + "type": "main", + "index": 0 + }, + { + "node": "Call DeepResearch API", + "type": "main", + "index": 0 + } ] ] }, "Call DeepResearch API": { - "main": [[{"node": "Process Research Result", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Process Research Result", + "type": "main", + "index": 0 + } + ] + ] }, "Process Research Result": { - "main": [[{"node": "Build CGP Envelope", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Build CGP Envelope", + "type": "main", + "index": 0 + } + ] + ] }, "Build CGP Envelope": { "main": [ [ - {"node": "Index to Hi-RAG", "type": "main", "index": 0}, - {"node": "Publish Result Event", "type": "main", "index": 0}, - {"node": "Auto Publish?", "type": "main", "index": 0} + { + "node": "Index to Hi-RAG", + "type": "main", + "index": 0 + }, + { + "node": "Publish Result Event", + "type": "main", + "index": 0 + }, + { + "node": "Auto Publish?", + "type": "main", + "index": 0 + } ] ] }, "Auto Publish?": { "main": [ - [{"node": "Trigger Content Generation", "type": "main", "index": 0}], + [ + { + "node": "Trigger Content Generation", + "type": "main", + "index": 0 + } + ], [] ] } @@ -190,9 +270,9 @@ "description": "DeepResearch orchestrator: triggers research, indexes results to Hi-RAG, publishes NATS events, optionally triggers content generation" }, "pinData": null, - "versionId": "deepresearch-v1", + "versionId": "8ab8e5c9-32d7-438a-96bb-cf47ffa24302", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "research", "deepresearch", "knowledge"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_ingestion_hub.json b/pmoves/n8n/flows/pmoves_ingestion_hub.json index 809fb8fea1..3e752a0da7 100644 --- a/pmoves/n8n/flows/pmoves_ingestion_hub.json +++ b/pmoves/n8n/flows/pmoves_ingestion_hub.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Detect Content Type", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Is Audio?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 100] + "position": [ + 720, + 100 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Is Video?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 300] + "position": [ + 720, + 300 + ] }, { "parameters": { @@ -75,7 +87,10 @@ "name": "Is Image?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 500] + "position": [ + 720, + 500 + ] }, { "parameters": { @@ -92,7 +107,10 @@ "name": "Trigger Audio Pipeline", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 100] + "position": [ + 980, + 100 + ] }, { "parameters": { @@ -109,7 +127,10 @@ "name": "Trigger Video Pipeline", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 300] + "position": [ + 980, + 300 + ] }, { "parameters": { @@ -126,7 +147,10 @@ "name": "Trigger Image Pipeline", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 500] + "position": [ + 980, + 500 + ] }, { "parameters": { @@ -136,36 +160,97 @@ "name": "Log Unhandled", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 700] + "position": [ + 980, + 700 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Detect Content Type", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Detect Content Type", + "type": "main", + "index": 0 + } + ] + ] }, "Detect Content Type": { - "main": [[ - {"node": "Is Audio?", "type": "main", "index": 0}, - {"node": "Is Video?", "type": "main", "index": 0}, - {"node": "Is Image?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Is Audio?", + "type": "main", + "index": 0 + }, + { + "node": "Is Video?", + "type": "main", + "index": 0 + }, + { + "node": "Is Image?", + "type": "main", + "index": 0 + } + ] + ] }, "Is Audio?": { "main": [ - [{"node": "Trigger Audio Pipeline", "type": "main", "index": 0}], - [{"node": "Is Video?", "type": "main", "index": 0}] + [ + { + "node": "Trigger Audio Pipeline", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Is Video?", + "type": "main", + "index": 0 + } + ] ] }, "Is Video?": { "main": [ - [{"node": "Trigger Video Pipeline", "type": "main", "index": 0}], - [{"node": "Is Image?", "type": "main", "index": 0}] + [ + { + "node": "Trigger Video Pipeline", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Is Image?", + "type": "main", + "index": 0 + } + ] ] }, "Is Image?": { "main": [ - [{"node": "Trigger Image Pipeline", "type": "main", "index": 0}], - [{"node": "Log Unhandled", "type": "main", "index": 0}] + [ + { + "node": "Trigger Image Pipeline", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Unhandled", + "type": "main", + "index": 0 + } + ] ] } }, @@ -176,9 +261,9 @@ "description": "Central dispatcher for all content ingestion - routes to appropriate analysis pipeline based on content type" }, "pinData": null, - "versionId": "ingestion-hub-v1", + "versionId": "60492d99-f1c2-46f1-9d09-a0fa3af8a383", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "ingestion", "routing"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_jellyfin_watcher.json b/pmoves/n8n/flows/pmoves_jellyfin_watcher.json index 8599b5eeab..df54c21799 100644 --- a/pmoves/n8n/flows/pmoves_jellyfin_watcher.json +++ b/pmoves/n8n/flows/pmoves_jellyfin_watcher.json @@ -14,7 +14,10 @@ "name": "Jellyfin Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Parse Jellyfin Event", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -42,7 +48,10 @@ "name": "Is Item Added?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -60,7 +69,10 @@ "name": "Is Playback Event?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -70,7 +82,10 @@ "name": "Build Ingest Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 200] + "position": [ + 980, + 200 + ] }, { "parameters": { @@ -87,7 +102,10 @@ "name": "Publish Item Added Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1240, 100] + "position": [ + 1240, + 100 + ] }, { "parameters": { @@ -105,7 +123,10 @@ "name": "Is Video Content?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1240, 300] + "position": [ + 1240, + 300 + ] }, { "parameters": { @@ -122,7 +143,10 @@ "name": "Trigger Video Analysis", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 200] + "position": [ + 1500, + 200 + ] }, { "parameters": { @@ -139,7 +163,10 @@ "name": "Trigger Audio Analysis", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 400] + "position": [ + 1500, + 400 + ] }, { "parameters": { @@ -149,7 +176,10 @@ "name": "Log Playback Event", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 400] + "position": [ + 980, + 400 + ] }, { "parameters": { @@ -166,45 +196,108 @@ "name": "Publish Playback Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1240, 400] + "position": [ + 1240, + 400 + ] } ], "connections": { "Jellyfin Webhook": { - "main": [[{"node": "Parse Jellyfin Event", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Parse Jellyfin Event", + "type": "main", + "index": 0 + } + ] + ] }, "Parse Jellyfin Event": { - "main": [[ - {"node": "Is Item Added?", "type": "main", "index": 0}, - {"node": "Is Playback Event?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Is Item Added?", + "type": "main", + "index": 0 + }, + { + "node": "Is Playback Event?", + "type": "main", + "index": 0 + } + ] + ] }, "Is Item Added?": { "main": [ - [{"node": "Build Ingest Request", "type": "main", "index": 0}], + [ + { + "node": "Build Ingest Request", + "type": "main", + "index": 0 + } + ], [] ] }, "Is Playback Event?": { "main": [ - [{"node": "Log Playback Event", "type": "main", "index": 0}], + [ + { + "node": "Log Playback Event", + "type": "main", + "index": 0 + } + ], [] ] }, "Build Ingest Request": { - "main": [[ - {"node": "Publish Item Added Event", "type": "main", "index": 0}, - {"node": "Is Video Content?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Publish Item Added Event", + "type": "main", + "index": 0 + }, + { + "node": "Is Video Content?", + "type": "main", + "index": 0 + } + ] + ] }, "Is Video Content?": { "main": [ - [{"node": "Trigger Video Analysis", "type": "main", "index": 0}], - [{"node": "Trigger Audio Analysis", "type": "main", "index": 0}] + [ + { + "node": "Trigger Video Analysis", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Trigger Audio Analysis", + "type": "main", + "index": 0 + } + ] ] }, "Log Playback Event": { - "main": [[{"node": "Publish Playback Event", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish Playback Event", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {}, @@ -214,9 +307,9 @@ "description": "Jellyfin event watcher: handles item additions and playback events, triggers analysis pipelines" }, "pinData": null, - "versionId": "jellyfin-watcher-v1", + "versionId": "c11fc268-081a-461b-8355-b6c8caa598ba", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "jellyfin", "media", "ingestion"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_notebook_content_feed.json b/pmoves/n8n/flows/pmoves_notebook_content_feed.json index 484f6075e3..2d753af4fe 100644 --- a/pmoves/n8n/flows/pmoves_notebook_content_feed.json +++ b/pmoves/n8n/flows/pmoves_notebook_content_feed.json @@ -14,7 +14,10 @@ "name": "Notebook Sync Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Parse Notebook Content", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Publish Sync Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Is Publishable?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -68,7 +80,10 @@ "name": "Generate Social Snippets", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 400] + "position": [ + 980, + 400 + ] }, { "parameters": { @@ -78,7 +93,10 @@ "name": "Create Studio Board Draft", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1240, 400] + "position": [ + 1240, + 400 + ] }, { "parameters": { @@ -87,21 +105,38 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "content-type", "value": "application/json"}, - {"name": "apikey", "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"}, - {"name": "Authorization", "value": "=Bearer {{$env.SUPABASE_SERVICE_ROLE_KEY}}"}, - {"name": "Prefer", "value": "return=representation"} + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "apikey", + "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}" + }, + { + "name": "Authorization", + "value": "=Bearer {{$env.SUPABASE_SERVICE_ROLE_KEY}}" + }, + { + "name": "Prefer", + "value": "return=representation" + } ] }, "sendBody": true, "body": "={{ JSON.stringify($json) }}", - "options": {"timeout": 10000} + "options": { + "timeout": 10000 + } }, "id": "save_draft", "name": "Save to Studio Board", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 400] + "position": [ + 1500, + 400 + ] }, { "parameters": { @@ -111,7 +146,10 @@ "name": "Build CGP Envelope", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [720, 600] + "position": [ + 720, + 600 + ] }, { "parameters": { @@ -128,7 +166,10 @@ "name": "Index to Hi-RAG", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 600] + "position": [ + 980, + 600 + ] }, { "parameters": { @@ -145,37 +186,100 @@ "name": "Publish Draft Created Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1760, 400] + "position": [ + 1760, + 400 + ] } ], "connections": { "Notebook Sync Webhook": { - "main": [[{"node": "Parse Notebook Content", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Parse Notebook Content", + "type": "main", + "index": 0 + } + ] + ] }, "Parse Notebook Content": { - "main": [[ - {"node": "Publish Sync Event", "type": "main", "index": 0}, - {"node": "Is Publishable?", "type": "main", "index": 0}, - {"node": "Build CGP Envelope", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Publish Sync Event", + "type": "main", + "index": 0 + }, + { + "node": "Is Publishable?", + "type": "main", + "index": 0 + }, + { + "node": "Build CGP Envelope", + "type": "main", + "index": 0 + } + ] + ] }, "Is Publishable?": { "main": [ - [{"node": "Generate Social Snippets", "type": "main", "index": 0}], + [ + { + "node": "Generate Social Snippets", + "type": "main", + "index": 0 + } + ], [] ] }, "Generate Social Snippets": { - "main": [[{"node": "Create Studio Board Draft", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Create Studio Board Draft", + "type": "main", + "index": 0 + } + ] + ] }, "Create Studio Board Draft": { - "main": [[{"node": "Save to Studio Board", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Save to Studio Board", + "type": "main", + "index": 0 + } + ] + ] }, "Save to Studio Board": { - "main": [[{"node": "Publish Draft Created Event", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish Draft Created Event", + "type": "main", + "index": 0 + } + ] + ] }, "Build CGP Envelope": { - "main": [[{"node": "Index to Hi-RAG", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Index to Hi-RAG", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {}, @@ -185,9 +289,9 @@ "description": "Open Notebook content feed: syncs notebook content, creates studio_board drafts, indexes to Hi-RAG" }, "pinData": null, - "versionId": "notebook-feed-v1", + "versionId": "ba51090c-fc13-42d1-9e00-a1b2468cec70", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "notebook", "content", "publishing"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_social_publisher.json b/pmoves/n8n/flows/pmoves_social_publisher.json index 28fb418bc7..b159ffe03c 100644 --- a/pmoves/n8n/flows/pmoves_social_publisher.json +++ b/pmoves/n8n/flows/pmoves_social_publisher.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Publish Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -34,7 +40,10 @@ "name": "Build Discord Payload", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -44,7 +53,10 @@ "name": "Build Twitter Thread", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -61,7 +73,10 @@ "name": "Post to Discord", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1000, 200] + "position": [ + 1000, + 200 + ] }, { "parameters": { @@ -79,7 +94,10 @@ "name": "Publish to Twitter?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1000, 400] + "position": [ + 1000, + 400 + ] }, { "parameters": { @@ -89,7 +107,10 @@ "name": "Post to Twitter", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1260, 400] + "position": [ + 1260, + 400 + ] }, { "parameters": { @@ -99,7 +120,10 @@ "name": "Collect Results", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1520, 300] + "position": [ + 1520, + 300 + ] }, { "parameters": { @@ -116,7 +140,10 @@ "name": "Publish Completion Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1780, 300] + "position": [ + 1780, + 300 + ] }, { "parameters": { @@ -133,7 +160,10 @@ "name": "Has Studio Board ID?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1780, 500] + "position": [ + 1780, + 500 + ] }, { "parameters": { @@ -142,59 +172,151 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "content-type", "value": "application/json"}, - {"name": "apikey", "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"}, - {"name": "Authorization", "value": "=Bearer {{$env.SUPABASE_SERVICE_ROLE_KEY}}"} + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "apikey", + "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}" + }, + { + "name": "Authorization", + "value": "=Bearer {{$env.SUPABASE_SERVICE_ROLE_KEY}}" + } ] }, "sendBody": true, "body": "={{ JSON.stringify({ status: 'published', meta: { published_at: new Date().toISOString(), platforms: $node['Collect Results'].json.platforms } }) }}", - "options": {"timeout": 10000} + "options": { + "timeout": 10000 + } }, "id": "update_studio", "name": "Update Studio Board", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2040, 500] + "position": [ + 2040, + 500 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Publish Request", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Publish Request", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Publish Request": { - "main": [[ - {"node": "Build Discord Payload", "type": "main", "index": 0}, - {"node": "Build Twitter Thread", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Build Discord Payload", + "type": "main", + "index": 0 + }, + { + "node": "Build Twitter Thread", + "type": "main", + "index": 0 + } + ] + ] }, "Build Discord Payload": { - "main": [[{"node": "Post to Discord", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Post to Discord", + "type": "main", + "index": 0 + } + ] + ] }, "Build Twitter Thread": { - "main": [[{"node": "Publish to Twitter?", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish to Twitter?", + "type": "main", + "index": 0 + } + ] + ] }, "Publish to Twitter?": { "main": [ - [{"node": "Post to Twitter", "type": "main", "index": 0}], - [{"node": "Collect Results", "type": "main", "index": 0}] + [ + { + "node": "Post to Twitter", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Collect Results", + "type": "main", + "index": 0 + } + ] ] }, "Post to Discord": { - "main": [[{"node": "Collect Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Collect Results", + "type": "main", + "index": 0 + } + ] + ] }, "Post to Twitter": { - "main": [[{"node": "Collect Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Collect Results", + "type": "main", + "index": 0 + } + ] + ] }, "Collect Results": { - "main": [[ - {"node": "Publish Completion Event", "type": "main", "index": 0}, - {"node": "Has Studio Board ID?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Publish Completion Event", + "type": "main", + "index": 0 + }, + { + "node": "Has Studio Board ID?", + "type": "main", + "index": 0 + } + ] + ] }, "Has Studio Board ID?": { "main": [ - [{"node": "Update Studio Board", "type": "main", "index": 0}], + [ + { + "node": "Update Studio Board", + "type": "main", + "index": 0 + } + ], [] ] } @@ -206,9 +328,9 @@ "description": "Multi-platform social publisher: Discord + Twitter/X with studio_board tracking" }, "pinData": null, - "versionId": "social-publisher-v1", + "versionId": "63087df4-4db9-4e52-82c7-023dfd56a347", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "publishing", "discord", "twitter", "social"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_video_analysis.json b/pmoves/n8n/flows/pmoves_video_analysis.json index a51b1a8d71..ec716a7a4e 100644 --- a/pmoves/n8n/flows/pmoves_video_analysis.json +++ b/pmoves/n8n/flows/pmoves_video_analysis.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Video Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Transcribe Audio Track", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 100] + "position": [ + 720, + 100 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Analyze Audio Track", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 300] + "position": [ + 720, + 300 + ] }, { "parameters": { @@ -75,7 +87,10 @@ "name": "Analyze Video Frames", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 500] + "position": [ + 720, + 500 + ] }, { "parameters": { @@ -85,7 +100,10 @@ "name": "Merge All Results", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1020, 300] + "position": [ + 1020, + 300 + ] }, { "parameters": { @@ -95,7 +113,10 @@ "name": "Build CGP Envelope", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1300, 300] + "position": [ + 1300, + 300 + ] }, { "parameters": { @@ -112,7 +133,10 @@ "name": "Index to Hi-RAG", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1560, 200] + "position": [ + 1560, + 200 + ] }, { "parameters": { @@ -129,7 +153,10 @@ "name": "Publish NATS Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1560, 400] + "position": [ + 1560, + 400 + ] }, { "parameters": { @@ -146,7 +173,10 @@ "name": "Auto Research?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1820, 300] + "position": [ + 1820, + 300 + ] }, { "parameters": { @@ -163,46 +193,119 @@ "name": "Trigger DeepResearch", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2080, 200] + "position": [ + 2080, + 200 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Video Request", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Video Request", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Video Request": { "main": [ [ - {"node": "Transcribe Audio Track", "type": "main", "index": 0}, - {"node": "Analyze Audio Track", "type": "main", "index": 0}, - {"node": "Analyze Video Frames", "type": "main", "index": 0} + { + "node": "Transcribe Audio Track", + "type": "main", + "index": 0 + }, + { + "node": "Analyze Audio Track", + "type": "main", + "index": 0 + }, + { + "node": "Analyze Video Frames", + "type": "main", + "index": 0 + } ] ] }, "Transcribe Audio Track": { - "main": [[{"node": "Merge All Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge All Results", + "type": "main", + "index": 0 + } + ] + ] }, "Analyze Audio Track": { - "main": [[{"node": "Merge All Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge All Results", + "type": "main", + "index": 0 + } + ] + ] }, "Analyze Video Frames": { - "main": [[{"node": "Merge All Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge All Results", + "type": "main", + "index": 0 + } + ] + ] }, "Merge All Results": { - "main": [[{"node": "Build CGP Envelope", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Build CGP Envelope", + "type": "main", + "index": 0 + } + ] + ] }, "Build CGP Envelope": { "main": [ [ - {"node": "Index to Hi-RAG", "type": "main", "index": 0}, - {"node": "Publish NATS Event", "type": "main", "index": 0}, - {"node": "Auto Research?", "type": "main", "index": 0} + { + "node": "Index to Hi-RAG", + "type": "main", + "index": 0 + }, + { + "node": "Publish NATS Event", + "type": "main", + "index": 0 + }, + { + "node": "Auto Research?", + "type": "main", + "index": 0 + } ] ] }, "Auto Research?": { "main": [ - [{"node": "Trigger DeepResearch", "type": "main", "index": 0}], + [ + { + "node": "Trigger DeepResearch", + "type": "main", + "index": 0 + } + ], [] ] } @@ -214,9 +317,9 @@ "description": "Video analysis pipeline: transcription + audio analysis + video frame analysis (YOLO/BLIP) + Hi-RAG indexing" }, "pinData": null, - "versionId": "video-analysis-v1", + "versionId": "d538835a-9758-4917-b70e-13153a4f1974", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "video", "analysis", "multimodal"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/telegram_voice_agent.json b/pmoves/n8n/flows/telegram_voice_agent.json index 86229b0f05..f61e4faf81 100644 --- a/pmoves/n8n/flows/telegram_voice_agent.json +++ b/pmoves/n8n/flows/telegram_voice_agent.json @@ -19,10 +19,10 @@ "webhookId": "telegram-voice-webhook" }, { - "parameters": { - "mode": "rules", - "rules": { - "values": [ + "parameters": { + "mode": "rules", + "rules": { + "values": [ { "conditions": { "options": { @@ -443,19 +443,6 @@ "versionId": "9b90a1d1-c767-428d-95c3-c1ff0cbec1c9", "versionCounter": 4, "triggerCount": 0, - "tags": [ - { - "updatedAt": "2025-12-12T13:18:03.530Z", - "createdAt": "2025-12-12T13:18:03.530Z", - "id": "0A3B8qR44ylHUdFz", - "name": "voice-agent" - }, - { - "updatedAt": "2025-12-12T13:18:07.141Z", - "createdAt": "2025-12-12T13:18:07.141Z", - "id": "qqizk7QbcLVI2tC1", - "name": "telegram" - } - ] + "tags": [] } ] diff --git a/pmoves/n8n/flows/voice_platform_router.json b/pmoves/n8n/flows/voice_platform_router.json index d3c5292e09..58cb2c373d 100644 --- a/pmoves/n8n/flows/voice_platform_router.json +++ b/pmoves/n8n/flows/voice_platform_router.json @@ -21,7 +21,7 @@ }, { "parameters": { - "jsCode": "// Unified platform normalizer\n// Input: raw webhook payload from any platform\n// Output: normalized voice message object\n\nconst items = $input.all();\nconst raw = items[0].json;\n// Webhook nodes typically nest the request body under `body`\nconst payload = raw.body ?? raw;\n\nlet normalized = {\n platform: null,\n user_id: null,\n user_name: null,\n channel_id: null,\n message_id: null,\n message_type: 'text', // 'text' | 'voice'\n content: null,\n audio_url: null,\n timestamp: new Date().toISOString(),\n raw_payload: raw,\n body: payload\n};\n\n// Detect platform and normalize\nif (payload.platform) {\n // Already normalized (direct API call)\n normalized = { ...normalized, ...payload };\n} else if (payload.message?.chat?.id && payload.message?.from?.id) {\n // Telegram format\n normalized.platform = 'telegram';\n normalized.user_id = String(payload.message.from.id);\n normalized.user_name = payload.message.from.first_name || payload.message.from.username;\n normalized.channel_id = String(payload.message.chat.id);\n normalized.message_id = String(payload.message.message_id);\n \n if (payload.message.voice) {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.message.voice.file_id; // Needs Telegram API to resolve\n } else if (payload.message.text) {\n normalized.message_type = 'text';\n normalized.content = payload.message.text;\n }\n} else if (payload.messages?.[0]?.from) {\n // WhatsApp format\n normalized.platform = 'whatsapp';\n normalized.user_id = payload.messages[0].from;\n normalized.user_name = payload.contacts?.[0]?.profile?.name || 'WhatsApp User';\n normalized.channel_id = payload.metadata?.phone_number_id;\n normalized.message_id = payload.messages[0].id;\n \n if (payload.messages[0].type === 'audio') {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.messages[0].audio?.id; // Needs WhatsApp API to resolve\n } else if (payload.messages[0].type === 'text') {\n normalized.message_type = 'text';\n normalized.content = payload.messages[0].text?.body;\n }\n} else if (payload.author?.id && payload.channelId) {\n // Discord format\n normalized.platform = 'discord';\n normalized.user_id = payload.author.id;\n normalized.user_name = payload.author.username;\n normalized.channel_id = payload.channelId;\n normalized.message_id = payload.id;\n \n if (payload.attachments?.length > 0 && payload.attachments[0].contentType?.startsWith('audio/')) {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.attachments[0].url;\n } else if (payload.content) {\n normalized.message_type = 'text';\n normalized.content = payload.content;\n }\n}\n\n// Ensure a stable message_id for contract publishing\nif (!normalized.message_id) {\n try {\n normalized.message_id = `local-${crypto.randomUUID()}`;\n } catch {\n normalized.message_id = `local-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n }\n}\n\n// Validate required fields\nif (!normalized.platform || !normalized.user_id) {\n throw new Error('Unable to detect platform or extract user_id from payload');\n}\n\nreturn [{ json: normalized }];" + "jsCode": "// Unified platform normalizer\n// Input: raw webhook payload from any platform\n// Output: normalized voice message object\n\nconst items = $input.all();\nconst raw = items[0].json;\n// Webhook nodes typically nest the request body under `body`\nconst payload = raw.body ?? raw;\n\nlet normalized = {\n platform: null,\n user_id: null,\n user_name: null,\n channel_id: null,\n message_id: null,\n message_type: 'text', // 'text' | 'voice'\n content: null,\n audio_url: null,\n timestamp: new Date().toISOString(),\n raw_payload: raw,\n body: payload\n};\n\n// Detect platform and normalize\nif (payload.platform) {\n // Already normalized (direct API call)\n normalized = { ...normalized, ...payload };\n} else if (payload.message?.chat?.id && payload.message?.from?.id) {\n // Telegram format\n normalized.platform = 'telegram';\n normalized.user_id = String(payload.message.from.id);\n normalized.user_name = payload.message.from.first_name || payload.message.from.username;\n normalized.channel_id = String(payload.message.chat.id);\n normalized.message_id = String(payload.message.message_id);\n \n if (payload.message.voice) {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.message.voice.file_id; // Needs Telegram API to resolve\n } else if (payload.message.text) {\n normalized.message_type = 'text';\n normalized.content = payload.message.text;\n }\n} else if (payload.messages?.[0]?.from) {\n // WhatsApp format\n normalized.platform = 'whatsapp';\n normalized.user_id = payload.messages[0].from;\n normalized.user_name = payload.contacts?.[0]?.profile?.name || 'WhatsApp User';\n normalized.channel_id = payload.metadata?.phone_number_id;\n normalized.message_id = payload.messages[0].id;\n \n if (payload.messages[0].type === 'audio') {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.messages[0].audio?.id; // Needs WhatsApp API to resolve\n } else if (payload.messages[0].type === 'text') {\n normalized.message_type = 'text';\n normalized.content = payload.messages[0].text?.body;\n }\n} else if (payload.author?.id && payload.channelId) {\n // Discord format\n normalized.platform = 'discord';\n normalized.user_id = payload.author.id;\n normalized.user_name = payload.author.username;\n normalized.channel_id = payload.channelId;\n normalized.message_id = payload.id;\n \n if (payload.attachments?.length > 0 && payload.attachments[0].contentType?.startsWith('audio/')) {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.attachments[0].url;\n } else if (payload.content) {\n normalized.message_type = 'text';\n normalized.content = payload.content;\n }\n}\n\n// Ensure a stable message_id for contract publishing\nif (!normalized.message_id) {\n try {\n normalized.message_id = `local-${crypto.randomUUID()}`;\n } catch {\n normalized.message_id = `local-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n }\n}\n\n// Validate required fields\nif (!normalized.platform || !normalized.user_id) {\n throw new Error('Unable to detect platform or extract user_id from payload');\n}\n\nreturn [{ json: normalized }];" }, "id": "normalize-platform", "name": "Normalize Platform", @@ -191,14 +191,14 @@ 900, 100 ], - "credentials": { - "telegramApi": { - "id": "telegram-bot-cred", - "name": "Telegram Bot" - } - }, - "disabled": true - }, + "credentials": { + "telegramApi": { + "id": "telegram-bot-cred", + "name": "Telegram Bot" + } + }, + "disabled": true + }, { "parameters": { "method": "GET", @@ -215,12 +215,12 @@ "name": "Telegram Download", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [ - 1100, - 100 - ], - "disabled": true - }, + "position": [ + 1100, + 100 + ], + "disabled": true + }, { "parameters": { "operation": "mediaUrlGet", @@ -234,14 +234,14 @@ 900, 200 ], - "credentials": { - "whatsAppBusinessCloudApi": { - "id": "whatsapp-cred", - "name": "WhatsApp Business" - } - }, - "disabled": true - }, + "credentials": { + "whatsAppBusinessCloudApi": { + "id": "whatsapp-cred", + "name": "WhatsApp Business" + } + }, + "disabled": true + }, { "parameters": { "method": "GET", @@ -264,14 +264,14 @@ 1100, 200 ], - "credentials": { - "httpHeaderAuth": { - "id": "whatsapp-auth", - "name": "WhatsApp Token" - } - }, - "disabled": true - }, + "credentials": { + "httpHeaderAuth": { + "id": "whatsapp-auth", + "name": "WhatsApp Token" + } + }, + "disabled": true + }, { "parameters": { "method": "GET", @@ -296,16 +296,16 @@ { "parameters": { "method": "POST", - "url": "http://flute-gateway:8055/v1/voice/recognize", + "url": "http://flute-gateway:8055/v1/voice/recognize", "sendBody": true, "contentType": "multipart-form-data", "bodyParameters": { "parameters": [ { "parameterType": "formBinaryData", - "name": "audio", + "name": "audio", "inputDataFieldName": "data" - } + } ] }, "options": {} @@ -333,17 +333,17 @@ 250 ] }, - { - "parameters": { - "method": "POST", - "url": "http://hi-rag-gateway-v2:8086/hirag/query", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"query\": \"{{ $json.content }}\",\n \"top_k\": 3,\n \"rerank\": true\n}", - "options": { - "timeout": 15000 - } - }, + { + "parameters": { + "method": "POST", + "url": "http://hi-rag-gateway-v2:8086/hirag/query", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"query\": \"{{ $json.content }}\",\n \"top_k\": 3,\n \"rerank\": true\n}", + "options": { + "timeout": 15000 + } + }, "id": "hirag-query", "name": "Hi-RAG Query", "type": "n8n-nodes-base.httpRequest", @@ -368,44 +368,43 @@ ] }, { - "parameters": { - "method": "POST", - "url": "={{ ($env.OPENAI_COMPATIBLE_BASE_URL || $env.OPENAI_COMPAT_BASE_URL || ($env.TENSORZERO_BASE_URL ? ($env.TENSORZERO_BASE_URL.replace(/\\/$/, '') + '/openai/v1') : 'https://api.openai.com/v1')).replace(/\\/$/, '') }}/chat/completions", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "content-type", - "value": "application/json" - }, - { - "name": "Authorization", - "value": "={{ 'Bearer ' + ($env.OPENAI_COMPAT_API_KEY || $env.OPENAI_API_KEY || '') }}" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ {\n model: ($env.VOICE_AGENT_MODEL || $env.OPENAI_MODEL || ($env.TENSORZERO_BASE_URL ? 'tensorzero::model_name::qwen2_5_14b' : 'gpt-4o-mini')),\n messages: [\n {\n role: 'system',\n content: 'You are a helpful voice assistant for PMOVES.AI. Keep responses concise and conversational, suitable for voice output. Aim for 1-3 sentences unless more detail is specifically requested.' + ($json.context || '')\n },\n {\n role: 'user',\n content: $json.content\n }\n ],\n max_tokens: 500\n} }}", - "options": { - "timeout": 180000 - } - }, + "parameters": { + "method": "POST", + "url": "={{ ($env.OPENAI_COMPATIBLE_BASE_URL || $env.OPENAI_COMPAT_BASE_URL || ($env.TENSORZERO_BASE_URL ? ($env.TENSORZERO_BASE_URL.replace(/\\/$/, '') + '/openai/v1') : 'https://api.openai.com/v1')).replace(/\\/$/, '') }}/chat/completions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "Authorization", + "value": "={{ 'Bearer ' + ($env.OPENAI_COMPAT_API_KEY || $env.OPENAI_API_KEY || '') }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ {\n model: ($env.VOICE_AGENT_MODEL || $env.OPENAI_MODEL || ($env.TENSORZERO_BASE_URL ? 'tensorzero::model_name::qwen2_5_14b' : 'gpt-4o-mini')),\n messages: [\n {\n role: 'system',\n content: 'You are a helpful voice assistant for PMOVES.AI. Keep responses concise and conversational, suitable for voice output. Aim for 1-3 sentences unless more detail is specifically requested.' + ($json.context || '')\n },\n {\n role: 'user',\n content: $json.content\n }\n ],\n max_tokens: 500\n} }}", + "options": { + "timeout": 180000 + } + }, "id": "llm-response", "name": "Generate Response", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [ - 2100, - 300 - ] - , - "continueOnFail": true, - "alwaysOutputData": true - }, + "position": [ + 2100, + 300 + ], + "continueOnFail": true, + "alwaysOutputData": true + }, { "parameters": { - "jsCode": "// Extract response and prepare for platform routing\nconst items = $input.all();\nconst rawResult = items[0].json;\nconst llmResult = rawResult.body ?? rawResult;\nconst prevData = $('Build Context').first().json;\n\nlet responseText = llmResult.choices?.[0]?.message?.content || llmResult.response || llmResult.text;\n\n// Fallback: if the LLM is unavailable, return the top RAG context snippet\nif (!responseText) {\n const ctx = String(prevData.context || '');\n const m = ctx.match(/\\[1\\]\\s*([^\\n]+)/);\n if (m && m[1] && m[1].trim()) {\n responseText = m[1].trim();\n }\n}\n\nif (!responseText) {\n responseText = 'Sorry, I could not generate a response.';\n}\n\nconst defaultModel = ($env.VOICE_AGENT_MODEL || $env.OPENAI_MODEL || ($env.TENSORZERO_BASE_URL ? 'tensorzero::model_name::qwen2_5_14b' : 'gpt-4o-mini'));\n\nreturn [{\n json: {\n ...prevData,\n response_text: responseText,\n model_used: llmResult.model || rawResult.model || defaultModel\n }\n}];" + "jsCode": "// Extract response and prepare for platform routing\nconst items = $input.all();\nconst rawResult = items[0].json;\nconst llmResult = rawResult.body ?? rawResult;\nconst prevData = $('Build Context').first().json;\n\nlet responseText = llmResult.choices?.[0]?.message?.content || llmResult.response || llmResult.text;\n\n// Fallback: if the LLM is unavailable, return the top RAG context snippet\nif (!responseText) {\n const ctx = String(prevData.context || '');\n const m = ctx.match(/\\[1\\]\\s*([^\\n]+)/);\n if (m && m[1] && m[1].trim()) {\n responseText = m[1].trim();\n }\n}\n\nif (!responseText) {\n responseText = 'Sorry, I could not generate a response.';\n}\n\nconst defaultModel = ($env.VOICE_AGENT_MODEL || $env.OPENAI_MODEL || ($env.TENSORZERO_BASE_URL ? 'tensorzero::model_name::qwen2_5_14b' : 'gpt-4o-mini'));\n\nreturn [{\n json: {\n ...prevData,\n response_text: responseText,\n model_used: llmResult.model || rawResult.model || defaultModel\n }\n}];" }, "id": "extract-response", "name": "Extract Response", @@ -416,13 +415,13 @@ 300 ] }, - { - "parameters": { - "mode": "rules", - "rules": { - "values": [ - { - "conditions": { + { + "parameters": { + "mode": "rules", + "rules": { + "values": [ + { + "conditions": { "options": { "leftValue": "", "caseSensitive": true, @@ -484,35 +483,35 @@ } ] }, - "renameOutput": true, - "outputKey": "discord" - }, - { - "conditions": { - "options": { - "leftValue": "", - "caseSensitive": true, - "typeValidation": "strict" - }, - "combinator": "and", - "conditions": [ - { - "leftValue": "={{ $json.platform }}", - "rightValue": "local", - "operator": { - "type": "string", - "operation": "equals" - } - } - ] - }, - "renameOutput": true, - "outputKey": "local" - } - ] - }, - "options": {} - }, + "renameOutput": true, + "outputKey": "discord" + }, + { + "conditions": { + "options": { + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "combinator": "and", + "conditions": [ + { + "leftValue": "={{ $json.platform }}", + "rightValue": "local", + "operator": { + "type": "string", + "operation": "equals" + } + } + ] + }, + "renameOutput": true, + "outputKey": "local" + } + ] + }, + "options": {} + }, "id": "route-platform-response", "name": "Route Platform (Response)", "type": "n8n-nodes-base.switch", @@ -538,14 +537,14 @@ 2700, 200 ], - "credentials": { - "telegramApi": { - "id": "telegram-bot-cred", - "name": "Telegram Bot" - } - }, - "disabled": true - }, + "credentials": { + "telegramApi": { + "id": "telegram-bot-cred", + "name": "Telegram Bot" + } + }, + "disabled": true + }, { "parameters": { "operation": "sendMessage", @@ -561,39 +560,39 @@ 2700, 300 ], - "credentials": { - "whatsAppBusinessCloudApi": { - "id": "whatsapp-cred", - "name": "WhatsApp Business" - } - }, - "disabled": true - }, - { - "parameters": { - "method": "POST", - "url": "http://messaging-gateway:8101/v1/send", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"platforms\": [\"discord\"],\n \"content\": \"{{ $json.response_text }}\",\n \"meta\": {\n \"channel_id\": \"{{ $json.channel_id }}\",\n \"message_id\": \"{{ $json.message_id }}\",\n \"user_id\": \"{{ $json.user_id }}\",\n \"user_name\": \"{{ $json.user_name }}\"\n }\n}", - "options": { - "timeout": 10000 - } - }, - "id": "send-discord", - "name": "Send Discord", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2700, - 400 - ], - "continueOnFail": true - }, + "credentials": { + "whatsAppBusinessCloudApi": { + "id": "whatsapp-cred", + "name": "WhatsApp Business" + } + }, + "disabled": true + }, + { + "parameters": { + "method": "POST", + "url": "http://messaging-gateway:8101/v1/send", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"platforms\": [\"discord\"],\n \"content\": \"{{ $json.response_text }}\",\n \"meta\": {\n \"channel_id\": \"{{ $json.channel_id }}\",\n \"message_id\": \"{{ $json.message_id }}\",\n \"user_id\": \"{{ $json.user_id }}\",\n \"user_name\": \"{{ $json.user_name }}\"\n }\n}", + "options": { + "timeout": 10000 + } + }, + "id": "send-discord", + "name": "Send Discord", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2700, + 400 + ], + "continueOnFail": true + }, { - "parameters": { - "method": "POST", - "url": "={{ $env.SUPABASE_URL }}/rest/v1/voice_messages", + "parameters": { + "method": "POST", + "url": "={{ $env.SUPABASE_URL }}/rest/v1/voice_messages", "sendHeaders": true, "headerParameters": { "parameters": [ @@ -615,13 +614,13 @@ } ] }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"platform\": \"{{ $json.platform }}\",\n \"user_id\": \"{{ $json.user_id }}\",\n \"user_name\": \"{{ $json.user_name }}\",\n \"transcript\": \"{{ $json.content }}\",\n \"response_text\": \"{{ $json.response_text }}\",\n \"model_used\": \"{{ $json.model_used }}\",\n \"status\": \"completed\",\n \"metadata\": { \"message_type\": \"{{ $json.message_type }}\", \"sources\": {{ JSON.stringify($json.sources || []) }} }\n}", - "options": { - "timeout": 10000 - } - }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"platform\": \"{{ $json.platform }}\",\n \"user_id\": \"{{ $json.user_id }}\",\n \"user_name\": \"{{ $json.user_name }}\",\n \"transcript\": \"{{ $json.content }}\",\n \"response_text\": \"{{ $json.response_text }}\",\n \"model_used\": \"{{ $json.model_used }}\",\n \"status\": \"completed\",\n \"metadata\": { \"message_type\": \"{{ $json.message_type }}\", \"sources\": {{ JSON.stringify($json.sources || []) }} }\n}", + "options": { + "timeout": 10000 + } + }, "id": "log-to-supabase", "name": "Log to Supabase", "type": "n8n-nodes-base.httpRequest", @@ -632,70 +631,70 @@ ], "continueOnFail": true }, - { - "parameters": { - "method": "POST", - "url": "={{ $env.AGENT_ZERO_BASE_URL || 'http://agent-zero:8080' }}/events/publish", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "content-type", - "value": "application/json" - }, - { - "name": "x-agent-token", - "value": "={{ $env.AGENT_ZERO_EVENTS_TOKEN || '' }}" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"topic\": \"voice.agent.response.v1\",\n \"payload\": {\n \"platform\": \"{{ $('Extract Response').first().json.platform }}\",\n \"user_id\": \"{{ $('Extract Response').first().json.user_id }}\",\n \"message_id\": \"{{ $('Extract Response').first().json.message_id }}\",\n \"response_text\": \"{{ $('Extract Response').first().json.response_text }}\",\n \"model_used\": \"{{ $('Extract Response').first().json.model_used }}\",\n \"timestamp\": \"{{ $('Extract Response').first().json.timestamp }}\",\n \"sources\": {{ JSON.stringify($('Extract Response').first().json.sources || []) }}\n }\n}", - "options": { - "timeout": 10000 - } - }, - "id": "publish-nats", - "name": "Publish NATS Event", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3100, - 300 - ], - "continueOnFail": true - }, - { - "parameters": { - "jsCode": "// Build a stable webhook response payload.\n// Prefer the pre-publish data from Extract Response; include publish result when available.\n\nlet base;\ntry {\n base = $('Extract Response').first().json;\n} catch {\n base = $input.first().json;\n}\n\nconst publishResult = $json || {};\n\nreturn [\n {\n json: {\n success: true,\n platform: base.platform,\n user_id: base.user_id,\n user_name: base.user_name,\n message_id: base.message_id,\n message_type: base.message_type,\n response_text: base.response_text,\n model_used: base.model_used,\n sources: base.sources || [],\n published_topic: publishResult.published || null,\n published_id: publishResult.id || null,\n publish_ok: Boolean(publishResult.published)\n }\n }\n];" - }, - "id": "build-webhook-response", - "name": "Build Webhook Response", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3300, - 300 - ] - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{ $json }}", - "options": {} - }, - "id": "respond-webhook", - "name": "Respond to Webhook", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.1, - "position": [ - 3500, - 300 - ] - } - ], - "connections": { + { + "parameters": { + "method": "POST", + "url": "={{ $env.AGENT_ZERO_BASE_URL || 'http://agent-zero:8080' }}/events/publish", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "x-agent-token", + "value": "={{ $env.AGENT_ZERO_EVENTS_TOKEN || '' }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"topic\": \"voice.agent.response.v1\",\n \"payload\": {\n \"platform\": \"{{ $('Extract Response').first().json.platform }}\",\n \"user_id\": \"{{ $('Extract Response').first().json.user_id }}\",\n \"message_id\": \"{{ $('Extract Response').first().json.message_id }}\",\n \"response_text\": \"{{ $('Extract Response').first().json.response_text }}\",\n \"model_used\": \"{{ $('Extract Response').first().json.model_used }}\",\n \"timestamp\": \"{{ $('Extract Response').first().json.timestamp }}\",\n \"sources\": {{ JSON.stringify($('Extract Response').first().json.sources || []) }}\n }\n}", + "options": { + "timeout": 10000 + } + }, + "id": "publish-nats", + "name": "Publish NATS Event", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3100, + 300 + ], + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "// Build a stable webhook response payload.\n// Prefer the pre-publish data from Extract Response; include publish result when available.\n\nlet base;\ntry {\n base = $('Extract Response').first().json;\n} catch {\n base = $input.first().json;\n}\n\nconst publishResult = $json || {};\n\nreturn [\n {\n json: {\n success: true,\n platform: base.platform,\n user_id: base.user_id,\n user_name: base.user_name,\n message_id: base.message_id,\n message_type: base.message_type,\n response_text: base.response_text,\n model_used: base.model_used,\n sources: base.sources || [],\n published_topic: publishResult.published || null,\n published_id: publishResult.id || null,\n publish_ok: Boolean(publishResult.published)\n }\n }\n];" + }, + "id": "build-webhook-response", + "name": "Build Webhook Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3300, + 300 + ] + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "id": "respond-webhook", + "name": "Respond to Webhook", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [ + 3500, + 300 + ] + } + ], + "connections": { "Webhook Trigger": { "main": [ [ @@ -882,38 +881,38 @@ ] ] }, - "Route Platform (Response)": { - "main": [ - [ - { - "node": "Send Telegram", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send WhatsApp", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Discord", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Log to Supabase", - "type": "main", - "index": 0 - } - ] - ] - }, + "Route Platform (Response)": { + "main": [ + [ + { + "node": "Send Telegram", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Send WhatsApp", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Send Discord", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log to Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, "Send Telegram": { "main": [ [ @@ -958,30 +957,29 @@ ] ] }, - "Publish NATS Event": { - "main": [ - [ - { - "node": "Build Webhook Response", - "type": "main", - "index": 0 - } - ] - ] - } - , - "Build Webhook Response": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - } - }, + "Publish NATS Event": { + "main": [ + [ + { + "node": "Build Webhook Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Webhook Response": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, "settings": { "executionOrder": "v1" }, @@ -994,25 +992,6 @@ "versionId": "9fe3a9e9-9cdc-4da6-8774-198b9e3d8219", "versionCounter": 4, "triggerCount": 0, - "tags": [ - { - "updatedAt": "2025-12-12T13:18:03.530Z", - "createdAt": "2025-12-12T13:18:03.530Z", - "id": "0A3B8qR44ylHUdFz", - "name": "voice-agent" - }, - { - "updatedAt": "2025-12-12T13:18:08.905Z", - "createdAt": "2025-12-12T13:18:08.905Z", - "id": "caJaL27AnB331apC", - "name": "router" - }, - { - "updatedAt": "2025-12-12T13:18:08.906Z", - "createdAt": "2025-12-12T13:18:08.906Z", - "id": "vPCBPRTNV0H4pHxT", - "name": "unified" - } - ] + "tags": [] } ] diff --git a/pmoves/n8n/flows/voice_shared_functions.json b/pmoves/n8n/flows/voice_shared_functions.json index 8fec73f75b..5044c968fe 100644 --- a/pmoves/n8n/flows/voice_shared_functions.json +++ b/pmoves/n8n/flows/voice_shared_functions.json @@ -79,10 +79,10 @@ "webhookId": "voice-tts" }, { - "parameters": { - "mode": "rules", - "rules": { - "values": [ + "parameters": { + "mode": "rules", + "rules": { + "values": [ { "conditions": { "options": { @@ -505,25 +505,6 @@ "versionId": "ea1b721c-4be4-40d4-a19a-0a7e4de2e12f", "versionCounter": 4, "triggerCount": 0, - "tags": [ - { - "updatedAt": "2025-12-12T13:18:03.530Z", - "createdAt": "2025-12-12T13:18:03.530Z", - "id": "0A3B8qR44ylHUdFz", - "name": "voice-agent" - }, - { - "updatedAt": "2025-12-12T13:18:03.534Z", - "createdAt": "2025-12-12T13:18:03.534Z", - "id": "pmKflb8eoRUftfSk", - "name": "functions" - }, - { - "updatedAt": "2025-12-12T13:18:03.533Z", - "createdAt": "2025-12-12T13:18:03.533Z", - "id": "uWvnDIE0g0A354hz", - "name": "shared" - } - ] + "tags": [] } ] diff --git a/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql b/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql new file mode 100644 index 0000000000..d2d5944c51 --- /dev/null +++ b/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql @@ -0,0 +1,49 @@ +create schema if not exists pmoves_core; + +create table if not exists pmoves_core.n8n_workflow_registry ( + workflow_id text primary key, + workflow_name text not null, + canonical_filename text, + canonical_path text, + source_repo text not null default 'PMOVES-n8n', + source_submodule_path text not null default 'PMOVES-n8n/workflows', + target_active boolean not null default false, + is_active boolean not null default false, + version_id uuid, + active_version_id uuid, + project_id text, + nodes_count integer not null default 0, + tags jsonb not null default '[]'::jsonb, + sync_meta jsonb not null default '{}'::jsonb, + last_synced_at timestamptz not null default timezone('utc', now()) +); + +create index if not exists n8n_workflow_registry_is_active_idx + on pmoves_core.n8n_workflow_registry (is_active); + +create index if not exists n8n_workflow_registry_target_active_idx + on pmoves_core.n8n_workflow_registry (target_active); + +grant usage on schema pmoves_core to authenticated, service_role; +grant select on pmoves_core.n8n_workflow_registry to authenticated; +grant select, insert, update, delete on pmoves_core.n8n_workflow_registry to service_role; + +alter table pmoves_core.n8n_workflow_registry enable row level security; + +drop policy if exists "n8n workflow registry read" on pmoves_core.n8n_workflow_registry; +create policy "n8n workflow registry read" + on pmoves_core.n8n_workflow_registry + for select + to authenticated + using (auth.role() = 'authenticated'); + +drop policy if exists "n8n workflow registry service write" on pmoves_core.n8n_workflow_registry; +create policy "n8n workflow registry service write" + on pmoves_core.n8n_workflow_registry + for all + to service_role + using (auth.role() = 'service_role') + with check (auth.role() = 'service_role'); + +comment on table pmoves_core.n8n_workflow_registry is + 'Live n8n workflow inventory synced from PMOVES-n8n via the n8n Public API.'; diff --git a/pmoves/tools/brand_defaults.py b/pmoves/tools/brand_defaults.py index 916db40ce7..d970663166 100644 --- a/pmoves/tools/brand_defaults.py +++ b/pmoves/tools/brand_defaults.py @@ -124,7 +124,8 @@ def _ensure_integration_credentials(text: str) -> str: text = _set_kv(text, "FIREFLY_APP_KEY", firefly_key) # n8n encryption key: 32-byte urlsafe token for workflow credential encryption. - # NOTE: N8N_API_KEY cannot be auto-generated — it must be created from the n8n UI. + # N8N_API_KEY is still not generated here because it requires a live n8n + # instance; use `make -C pmoves n8n-api-bootstrap` after bring-up. n8n_enc = _get_kv(text, "N8N_ENCRYPTION_KEY") if _is_blank_or_placeholder(n8n_enc): text = _set_kv(text, "N8N_ENCRYPTION_KEY", _strong_random(32))