From 87dbd3e6ea1719824b5c697ec8b806d757b82c65 Mon Sep 17 00:00:00 2001 From: Paccoco Date: Sun, 10 May 2026 09:52:48 -0500 Subject: [PATCH] Add OpenClaw to AI stack --- ai/.env.example | 3 + ai/docker-compose.yaml | 26 ++ docs/architecture/SERVICES_DIRECTORY.md | 6 +- .../14-paperless-inbox-reminder-v1.2.json | 293 +++++++++++++++++ n8n-workflows/15-n8n-health-monitor-v1.2.json | 302 ++++++++++++++++++ n8n-workflows/TESTING-STATUS.md | 88 +++++ 6 files changed, 716 insertions(+), 2 deletions(-) create mode 100755 n8n-workflows/14-paperless-inbox-reminder-v1.2.json create mode 100755 n8n-workflows/15-n8n-health-monitor-v1.2.json create mode 100755 n8n-workflows/TESTING-STATUS.md diff --git a/ai/.env.example b/ai/.env.example index 16a7dd2..42c3b35 100644 --- a/ai/.env.example +++ b/ai/.env.example @@ -8,3 +8,6 @@ OPENWEBUI_DB_PASS=changeme # Scriberr — Hugging Face token for model downloads HF_TOKEN=changeme + +# OpenClaw — generate with: openssl rand -hex 32 +OPENCLAW_GATEWAY_TOKEN=changeme diff --git a/ai/docker-compose.yaml b/ai/docker-compose.yaml index 3223d47..45231b0 100644 --- a/ai/docker-compose.yaml +++ b/ai/docker-compose.yaml @@ -128,3 +128,29 @@ services: - driver: nvidia count: 1 capabilities: [gpu] + + openclaw: + container_name: openclaw + image: ghcr.io/openclaw/openclaw:latest + restart: unless-stopped + networks: + - ai-net + - ai-services + - pangolin + ports: + - "18789:18789" + environment: + TZ: ${TZ} + OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} + OPENCLAW_DISABLE_BONJOUR: "1" + volumes: + - /mnt/docker-ssd/docker/appdata/openclaw/config:/home/node/.openclaw + - /mnt/docker-ssd/docker/appdata/openclaw/workspace:/home/node/.openclaw/workspace + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:18789/healthz || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s diff --git a/docs/architecture/SERVICES_DIRECTORY.md b/docs/architecture/SERVICES_DIRECTORY.md index b965b34..38aa9c2 100644 --- a/docs/architecture/SERVICES_DIRECTORY.md +++ b/docs/architecture/SERVICES_DIRECTORY.md @@ -2,7 +2,7 @@ All homelab services, their hosts, ports, and current status. -*Last updated: 2026-05-09* +*Last updated: 2026-05-10* ## Shared Databases (PD) @@ -32,8 +32,10 @@ All homelab services, their hosts, ports, and current status. | LiteLLM | PD | 4000 | ✅ Active | | OpenWebUI | PD | 8282 | ✅ Active | | Qdrant | PD | 6333 (HTTP) / 6334 (gRPC) | ✅ Active | -| Whisper (faster-whisper) | PD | 8786 | ✅ Active | +| Whisper (faster-whisper, CPU) | N.O.M.A.D. (10.5.1.16) | 8786 | ✅ Active | +| Whisper (faster-whisper, CUDA large-v3) | Rocinante (10.5.1.112) | 8787 | ✅ Active | | SearXNG | PD | 8888 | ✅ Active | +| OpenClaw | PD | 18789 | ✅ Active | | Ollama — light tier | N.O.M.A.D. (10.5.1.16) | 11434 | ✅ Active | | Ollama — heavy tier | Rocinante (10.5.1.112) | 11434 | ✅ Active | | Reranker (TEI) | Serenity (10.5.1.5) | 9787 | ✅ Active | diff --git a/n8n-workflows/14-paperless-inbox-reminder-v1.2.json b/n8n-workflows/14-paperless-inbox-reminder-v1.2.json new file mode 100755 index 0000000..49e5eaf --- /dev/null +++ b/n8n-workflows/14-paperless-inbox-reminder-v1.2.json @@ -0,0 +1,293 @@ +{ + "name": "Paperless Inbox Reminder v1.2", + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "weeksInterval": 1, + "triggerAtDay": [ + 1 + ], + "triggerAtHour": 9, + "triggerAtMinute": 0 + } + ] + } + }, + "id": "41404371-29ed-4660-9989-a5cbd890555b", + "name": "Every Monday 9 AM", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 0, + 0 + ] + }, + { + "parameters": { + "method": "GET", + "url": "https://paperless.paccoco.com/api/documents/", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "=Token {{ $env.PAPERLESS_API_TOKEN }}" + } + ] + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "is_tagged", + "value": "0" + }, + { + "name": "page_size", + "value": "25" + }, + { + "name": "ordering", + "value": "created" + }, + { + "name": "fields", + "value": "id,title,created,document_type,tags" + } + ] + }, + "options": {} + }, + "id": "fdcd5728-6d27-445f-961b-18f204297469", + "name": "Fetch Inbox Docs", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 224, + 0 + ] + }, + { + "parameters": { + "jsCode": "const data = $input.first().json;\nconst docs = data.results || [];\nconst total = data.count || 0;\n\n// Oldest 5 for the reminder body\nconst oldest = docs.slice(0, 5).map(d => {\n const date = d.created ? d.created.substring(0, 10) : 'unknown date';\n return ` • ${d.title || 'Untitled'} (added ${date})`;\n});\n\nreturn [{\n json: {\n total,\n hasItems: total > 0,\n hasItemsStr: total > 0 ? 'yes' : 'no',\n oldestList: oldest.join('\\n'),\n remaining: Math.max(0, total - 5)\n }\n}];" + }, + "id": "8fbe6be0-1ed3-4126-8f21-c3f4efb161c6", + "name": "Parse Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 448, + 0 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "b206599d-0c79-4a32-9fb9-91e1c41dcff2", + "leftValue": "={{ $json.hasItemsStr }}", + "rightValue": "yes", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + } + }, + "id": "e24344a1-e857-462c-b96a-842169e83a6d", + "name": "Has Inbox Items?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 672, + 0 + ] + }, + { + "parameters": { + "jsCode": "const d = $input.first().json;\nconst extra = d.remaining > 0 ? `\\n … and ${d.remaining} more` : '';\nconst body = `${d.total} untagged document${d.total !== 1 ? 's' : ''} in Paperless inbox:\\n${d.oldestList}${extra}\\n\\nhttps://paperless.paccoco.com`;\nreturn [{ json: { title: '📥 Paperless Inbox Needs Attention', message: body, priority: 5 } }];" + }, + "id": "897af9c9-4429-445f-8160-a03a14a318a3", + "name": "Format Reminder", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 896, + -120 + ] + }, + { + "parameters": { + "jsCode": "return [{ json: { title: '✅ Paperless Inbox Clear', message: 'No untagged documents — inbox is clean.', priority: 1 } }];" + }, + "id": "bd8fe88e-7876-45dd-8344-983674fba79d", + "name": "Format Clear", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 896, + 120 + ] + }, + { + "parameters": { + "message": "={{ $json.message }}", + "additionalFields": { + "priority": "={{ $json.priority }}", + "title": "={{ $json.title }}" + }, + "options": { + "contentType": "text/plain" + } + }, + "id": "74860e7f-42e3-448f-9e46-5cfb6f12cdc2", + "name": "Push to Gotify (Reminder)", + "type": "n8n-nodes-base.gotify", + "typeVersion": 1, + "position": [ + 1120, + -120 + ], + "credentials": { + "gotifyApi": { + "id": "SETUP_REQUIRED", + "name": "Reminders" + } + } + }, + { + "parameters": { + "message": "={{ $json.message }}", + "additionalFields": { + "priority": "={{ $json.priority }}", + "title": "={{ $json.title }}" + }, + "options": { + "contentType": "text/plain" + } + }, + "id": "b1dbd39b-87c3-43ec-9cec-f63eabbd0fa5", + "name": "Push to Gotify (Clear)", + "type": "n8n-nodes-base.gotify", + "typeVersion": 1, + "position": [ + 1120, + 120 + ], + "credentials": { + "gotifyApi": { + "id": "SETUP_REQUIRED", + "name": "Reminders" + } + } + } + ], + "connections": { + "Every Monday 9 AM": { + "main": [ + [ + { + "node": "Fetch Inbox Docs", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch Inbox Docs": { + "main": [ + [ + { + "node": "Parse Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Response": { + "main": [ + [ + { + "node": "Has Inbox Items?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has Inbox Items?": { + "main": [ + [ + { + "node": "Format Reminder", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Format Clear", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Reminder": { + "main": [ + [ + { + "node": "Push to Gotify (Reminder)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Clear": { + "main": [ + [ + { + "node": "Push to Gotify (Clear)", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "staticData": null, + "meta": null, + "pinData": {}, + "tags": [ + { + "name": "reminders" + }, + { + "name": "paperless" + }, + { + "name": "scheduled" + } + ], + "id": "2b80579b-8d89-42d5-8b8b-9137f59ea206" +} diff --git a/n8n-workflows/15-n8n-health-monitor-v1.2.json b/n8n-workflows/15-n8n-health-monitor-v1.2.json new file mode 100755 index 0000000..3e3e720 --- /dev/null +++ b/n8n-workflows/15-n8n-health-monitor-v1.2.json @@ -0,0 +1,302 @@ +{ + "name": "n8n Self-Health Monitor v1.2", + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "minutesInterval": 15 + } + ] + } + }, + "id": "dabcb0ae-d513-410e-b779-4570df5c4a35", + "name": "Every 15 Minutes", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [0, 0] + }, + { + "parameters": { + "method": "GET", + "url": "http://10.5.1.6:5678/api/v1/workflows", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-N8N-API-KEY", + "value": "={{ $env.N8N_API_KEY }}" + } + ] + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "limit", + "value": "250" + } + ] + }, + "options": {} + }, + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "Fetch All Workflows", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [224, 0] + }, + { + "parameters": { + "method": "GET", + "url": "http://10.5.1.6:5678/api/v1/executions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-N8N-API-KEY", + "value": "={{ $env.N8N_API_KEY }}" + } + ] + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "status", + "value": "error" + }, + { + "name": "includeData", + "value": "false" + }, + { + "name": "limit", + "value": "50" + } + ] + }, + "options": {} + }, + "id": "806d75e9-b95a-499a-b997-a7f7dc046775", + "name": "Fetch Failed Executions", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [448, 0] + }, + { + "parameters": { + "jsCode": "const data = $input.first().json;\nconst executions = data.data || [];\nconst oneHourAgo = Date.now() - (60 * 60 * 1000);\n\n// Build workflow ID → name lookup from previously fetched workflows\nconst wfResponse = $('Fetch All Workflows').first().json;\nconst nameMap = {};\n(wfResponse.data || []).forEach(wf => { nameMap[wf.id] = wf.name; });\n\n// Filter to failures in the past hour\nconst recent = executions.filter(e => {\n const ts = new Date(e.startedAt || e.stoppedAt || 0).getTime();\n return ts >= oneHourAgo;\n});\n\n// Map executions to failure objects, resolving names from the lookup\nconst failures = recent.map(e => ({\n executionId: e.id,\n workflowId: e.workflowId,\n workflowName: nameMap[e.workflowId] || e.workflowData?.name || `Workflow ${e.workflowId}`,\n startedAt: e.startedAt,\n stoppedAt: e.stoppedAt,\n errorMessage: e.data?.resultData?.error?.message || 'Unknown error'\n}));\n\n// Deduplicate by workflowName (report once per workflow per check)\nconst seen = new Set();\nconst unique = failures.filter(f => {\n if (seen.has(f.workflowName)) return false;\n seen.add(f.workflowName);\n return true;\n});\n\nreturn [{ json: { hasFailures: unique.length > 0, hasFailuresStr: unique.length > 0 ? 'yes' : 'no', count: unique.length, failures: unique } }];" + }, + "id": "f5d47910-5047-4c4f-aa84-a6907aa38551", + "name": "Filter Recent Failures", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [672, 0] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "02ff82df-a31d-491b-9cd1-96874c8ae2fe", + "leftValue": "={{ $json.hasFailuresStr }}", + "rightValue": "yes", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + } + }, + "id": "7c298433-ab89-435e-8955-73da6b986f46", + "name": "Has Failures?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [896, 0] + }, + { + "parameters": { + "jsCode": "const d = $input.first().json;\n// Fingerprint = sorted workflow names joined — stable across retriggers\nconst fingerprint = (d.failures || [])\n .map(f => f.workflowName)\n .sort()\n .join('|') || 'unknown';\nreturn [{ json: { ...d, fingerprint } }];" + }, + "id": "d3609d45-ba9f-41d3-a9bd-8c558a719f10", + "name": "Build Fingerprint", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1120, -120] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "SELECT fingerprint, last_seen FROM n8n_health_dedup WHERE fingerprint = '{{ $json.fingerprint }}' AND last_seen > NOW() - INTERVAL '1 hour' LIMIT 1;", + "options": {} + }, + "id": "15a3dd71-bc5b-4396-9534-63518e097438", + "name": "Lookup Dedup", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.5, + "position": [1344, -120], + "credentials": { + "postgres": { + "id": "n9svoXemqSZoNNUB", + "name": "Postgres account" + } + }, + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const alertData = $('Build Fingerprint').first().json;\nconst rows = $input.all().filter(r => r.json && r.json.fingerprint);\nconst alreadyNotified = rows.length > 0;\nreturn [{ json: { ...alertData, alreadyNotified, shouldNotify: !alreadyNotified, shouldNotifyStr: !alreadyNotified ? 'yes' : 'no' } }];" + }, + "id": "a92590e3-7044-4ba3-9b39-f6a9457c5fbe", + "name": "Check Already Notified", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1568, -120] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "aa5db0c3-75a7-4ea4-b29e-c12568ccf320", + "leftValue": "={{ $json.shouldNotifyStr }}", + "rightValue": "yes", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + } + }, + "id": "39ac0eaf-73ce-4fe1-9c4b-d09ac920291d", + "name": "Should Notify?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [1792, -120] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO n8n_health_dedup (fingerprint, last_seen, workflow_names) VALUES ('{{ $json.fingerprint }}', NOW(), '{{ $json.failures.map(f => f.workflowName).join(\", \") }}') ON CONFLICT (fingerprint) DO UPDATE SET last_seen = NOW(), workflow_names = EXCLUDED.workflow_names;", + "options": {} + }, + "id": "0c3be155-0c3a-4354-b863-73a3f5bbf598", + "name": "Upsert Dedup Record", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.5, + "position": [2016, -120], + "credentials": { + "postgres": { + "id": "n9svoXemqSZoNNUB", + "name": "Postgres account" + } + } + }, + { + "parameters": { + "jsCode": "const d = $('Build Fingerprint').first().json;\nconst lines = (d.failures || []).map(f =>\n ` • ${f.workflowName}\\n ${f.errorMessage.substring(0, 120)}`\n);\nconst body = `${d.count} workflow${d.count !== 1 ? 's' : ''} failed in the past hour:\\n\\n${lines.join('\\n')}\\n\\nhttps://n8n.paccoco.com`;\nreturn [{ json: { title: `⚠️ n8n: ${d.count} Workflow Failure${d.count !== 1 ? 's' : ''}`, message: body, priority: 7 } }];" + }, + "id": "1c6b42bf-a38e-47b6-82ac-50c43ee9e583", + "name": "Format Notification", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2240, -120] + }, + { + "parameters": { + "message": "={{ $json.message }}", + "additionalFields": { + "priority": "={{ $json.priority }}", + "title": "={{ $json.title }}" + }, + "options": { + "contentType": "text/plain" + } + }, + "id": "09435c2f-9044-432a-8ca8-dd3ce3262c76", + "name": "Push to Gotify", + "type": "n8n-nodes-base.gotify", + "typeVersion": 1, + "position": [2464, -120], + "credentials": { + "gotifyApi": { + "id": "SETUP_REQUIRED", + "name": "n8n Health" + } + } + } + ], + "connections": { + "Every 15 Minutes": { + "main": [[{ "node": "Fetch All Workflows", "type": "main", "index": 0 }]] + }, + "Fetch All Workflows": { + "main": [[{ "node": "Fetch Failed Executions", "type": "main", "index": 0 }]] + }, + "Fetch Failed Executions": { + "main": [[{ "node": "Filter Recent Failures", "type": "main", "index": 0 }]] + }, + "Filter Recent Failures": { + "main": [[{ "node": "Has Failures?", "type": "main", "index": 0 }]] + }, + "Has Failures?": { + "main": [ + [{ "node": "Build Fingerprint", "type": "main", "index": 0 }], + [] + ] + }, + "Build Fingerprint": { + "main": [[{ "node": "Lookup Dedup", "type": "main", "index": 0 }]] + }, + "Lookup Dedup": { + "main": [[{ "node": "Check Already Notified", "type": "main", "index": 0 }]] + }, + "Check Already Notified": { + "main": [[{ "node": "Should Notify?", "type": "main", "index": 0 }]] + }, + "Should Notify?": { + "main": [ + [{ "node": "Upsert Dedup Record", "type": "main", "index": 0 }], + [] + ] + }, + "Upsert Dedup Record": { + "main": [[{ "node": "Format Notification", "type": "main", "index": 0 }]] + }, + "Format Notification": { + "main": [[{ "node": "Push to Gotify", "type": "main", "index": 0 }]] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "staticData": null, + "meta": null, + "pinData": {}, + "tags": [ + { "name": "monitoring" }, + { "name": "scheduled" }, + { "name": "n8n" } + ], + "id": "febfda9b-0f4e-40b5-ab84-82ba9b7fcf29" +} diff --git a/n8n-workflows/TESTING-STATUS.md b/n8n-workflows/TESTING-STATUS.md new file mode 100755 index 0000000..c85915a --- /dev/null +++ b/n8n-workflows/TESTING-STATUS.md @@ -0,0 +1,88 @@ +# n8n Workflow Testing Status +_Last updated: 2026-05-10_ + +--- + +## ✅ Confirmed Working + +| # | Workflow | Version | Notes | +|---|----------|---------|-------| +| 01 | Grafana Alert → AI + Gotify | v1.4 | Dedup + recovery branch confirmed | +| 08 | Class Recording RAG | v1.2 | Paperless + Gotify end-to-end confirmed | +| 09 | Class Transcript Ingest | v1.1 | Gotify confirmed | +| 14 | Paperless Inbox Reminder | v1.2 | Fixed: `Bearer` → `Token` auth; fixed: `hasItemsStr` bug | +| 15 | n8n Health Monitor | v1.2 | Fixed: workflow names (was showing IDs); Postgres dedup working | +| 16 | NFS Watchdog + Auto-Heal | v1.2 | Confirmed working | +| 07 | Git Commit → AI Summary → Gotify | v1.0 | Confirmed working | + +## ⏸ Retired + +| # | Workflow | Reason | +|---|----------|--------| +| 04 | Whisper Transcription | Replaced — transcription now runs via Docker on Rocinante (RTX 4090) and sends directly to n8n | + +--- + +## 🔲 Not Yet Tested + +### 02 — RAG Pipeline v1.0 +**What it does:** Two webhooks — `POST /rag/ingest` chunks + embeds text into Qdrant; `POST /rag/query` embeds query, searches Qdrant, asks LiteLLM, returns answer. + +**Pre-test checklist:** +- [ ] `knowledge_base` collection exists in Qdrant (`GET http://10.5.1.6:6333/collections`) +- [ ] `nomic-embed-text` model pulled in Ollama (`ollama list` on PD) +- [ ] **Hardcoded LiteLLM key** in `LiteLLM Answer` node — should use `$env.LITELLM_API_KEY` + +**Test approach:** POST `{"text": "some test content", "source": "test"}` to `/rag/ingest`, then POST `{"query": "test content question"}` to `/rag/query` and verify an answer comes back. + +--- + +### 03 — Paperless AI Processing v1.0 +**What it does:** Paperless webhook on new document → AI classifies (type/summary/tags) → adds note to doc + sends Gotify + applies tags (parallel branches after `Parse Analysis`). + +**Pre-test checklist:** +- [ ] Paperless webhook configured to POST to `https://n8n.paccoco.com/webhook/paperless/new-document` on document add +- [ ] **Hardcoded Paperless token** in `Fetch Document`, `Add Note to Document`, and inline in `Apply Suggested Tags to Paperless` code — should use `$env.PAPERLESS_API_TOKEN` +- [ ] **Hardcoded LiteLLM key** in `AI Classify & Extract` — should use `$env.LITELLM_API_KEY` +- [ ] Gotify credential `ajvRjvj0QldLQYmo` ("Paperless") — already wired ✓ + +**Test approach:** Upload a test document to Paperless, then manually POST `{"document_id": }` to the webhook to trigger processing. Check Paperless for note + tags, check Gotify for notification. + +--- + +### 05 — Paperless → RAG v1.0 +**What it does:** Webhook on `POST /paperless/rag-ingest` → fetches Paperless doc → chunks + embeds content → upserts to Qdrant `knowledge_base` → Gotify notification. + +**Pre-test checklist:** +- [ ] `knowledge_base` collection exists in Qdrant (same as 02) +- [ ] **Hardcoded Paperless token** in `Fetch Document` — should use `$env.PAPERLESS_API_TOKEN` +- [ ] Gotify credential `SETUP_REQUIRED` ("Paperless RAG") — needs wiring + +**Test approach:** POST `{"document_id": }` to the webhook. Verify chunks appear in Qdrant and Gotify notification fires. + +--- + +### 06 — RSS Digest v1.0 +**What it does:** Every 6 hours (+ manual webhook + manual trigger), fetches 33 RSS feeds, parses items, deduplicates via Postgres (`rss_seen_items` table), scores by keyword relevance/urgency, AI-summarizes new items per feed, pushes per-feed digests to Gotify. + +**Pre-test checklist:** +- [ ] `rss_seen_items` table exists in Postgres +- [ ] **Hardcoded LiteLLM key** in `AI Summarize Feed` node — should use `$env.LITELLM_API_KEY` +- [ ] Postgres credential `n9svoXemqSZoNNUB` — already wired ✓ +- [ ] Gotify credential `ExUx3oqYQyhkqZcp` ("Rss Feed") — already wired ✓ + +**Test approach:** Use the "When clicking Execute workflow" manual trigger. First run will insert all items as new (expect lots of Gotify notifications). Second run should be silent (all deduped). + +--- + +## 🐛 Known Issues / Tech Debt + +| Workflow | Issue | Priority | +|----------|-------|----------| +| 02 | Hardcoded LiteLLM API key | Medium | +| 03 | Hardcoded LiteLLM API key + Paperless token | Medium | +| 05 | Hardcoded Paperless token | Medium | +| 06 | Hardcoded LiteLLM API key | Medium | +| 07 | Hardcoded LiteLLM API key (working but tech debt) | Low | +| 15 | `includeData: false` means error messages show as "Unknown error" | Low | +| 09 v1.0 | Still active in n8n alongside v1.1 — should be deactivated | Low |