diff --git a/automation/.env.example b/automation/.env.example
index a345f34..7a99aaa 100644
--- a/automation/.env.example
+++ b/automation/.env.example
@@ -28,6 +28,13 @@ UNIFI_PASSWORD=***
UNIFI_SITE=default
UNIFI_VERIFY_TLS=false
+# Pangolin integration API
+PANGOLIN_API_URL=https://api.paccoco.com/v1
+PANGOLIN_API_KEY=CHANGE_ME
+PANGOLIN_ORG_ID=paccoco
+PANGOLIN_SITE_ID=4
+PANGOLIN_DOMAIN_ID=domain1
+
# Backup automation scaffolding
SERENITY_BACKUP_HOST=root@10.5.30.5
SERENITY_BACKUP_ROOT=/mnt/user/backups/plausible-deniability
diff --git a/automation/bin/pangolin_upsert_dispatcharr.py b/automation/bin/pangolin_upsert_dispatcharr.py
new file mode 100644
index 0000000..7e20313
--- /dev/null
+++ b/automation/bin/pangolin_upsert_dispatcharr.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+"""Create or update the Pangolin resource/target for Dispatcharr.
+
+Expected env vars:
+- PANGOLIN_API_URL e.g. https://api.paccoco.com/v1
+- PANGOLIN_API_KEY bearer token for Pangolin integration API
+- PANGOLIN_ORG_ID e.g. paccoco
+Optional env defaults:
+- PANGOLIN_SITE_ID default 4 (Plausible Deniability)
+- PANGOLIN_DOMAIN_ID default domain1
+
+Default desired resource:
+- host/domain: tv.paccoco.com
+- site: Plausible Deniability (siteId 4)
+- target: dispatcharr:9191
+- Pangolin auth: disabled (sso=false)
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+from typing import Any
+from urllib import error, parse, request
+
+DEFAULT_API_URL = "https://api.paccoco.com/v1"
+DEFAULT_ORG_ID = "paccoco"
+DEFAULT_DOMAIN_ID = "domain1"
+DEFAULT_SITE_ID = 4
+DEFAULT_NAME = "dispatcharr"
+DEFAULT_SUBDOMAIN = "tv"
+DEFAULT_TARGET_IP = "dispatcharr"
+DEFAULT_TARGET_PORT = 9191
+USER_AGENT = "doris-pangolin-dispatcharr/1.0"
+
+
+def env(name: str, default: str | None = None) -> str | None:
+ value = os.environ.get(name)
+ if value is None or value == "":
+ return default
+ return value
+
+
+def api_request(base_url: str, api_key: str, method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, Any]:
+ url = base_url.rstrip("/") + path
+ headers = {
+ "Authorization": f"Bearer {api_key}",
+ "Accept": "application/json",
+ "User-Agent": USER_AGENT,
+ }
+ data = None
+ if body is not None:
+ data = json.dumps(body).encode("utf-8")
+ headers["Content-Type"] = "application/json"
+ req = request.Request(url, data=data, headers=headers, method=method.upper())
+ try:
+ with request.urlopen(req, timeout=30) as resp:
+ raw = resp.read().decode("utf-8", "replace")
+ return resp.status, json.loads(raw) if raw else None
+ except error.HTTPError as exc:
+ raw = exc.read().decode("utf-8", "replace")
+ try:
+ payload = json.loads(raw)
+ except Exception:
+ payload = raw
+ return exc.code, payload
+
+
+def fail(msg: str, payload: Any = None) -> int:
+ print(msg, file=sys.stderr)
+ if payload is not None:
+ if isinstance(payload, (dict, list)):
+ print(json.dumps(payload, indent=2, sort_keys=True), file=sys.stderr)
+ else:
+ print(payload, file=sys.stderr)
+ return 1
+
+
+def find_resource(resources: list[dict[str, Any]], *, name: str, full_domain: str) -> dict[str, Any] | None:
+ for resource in resources:
+ if resource.get("fullDomain") == full_domain:
+ return resource
+ for resource in resources:
+ if resource.get("name") == name:
+ return resource
+ return None
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Upsert Pangolin Dispatcharr resource")
+ parser.add_argument("--api-url", default=env("PANGOLIN_API_URL", DEFAULT_API_URL))
+ parser.add_argument("--api-key", default=env("PANGOLIN_API_KEY"))
+ parser.add_argument("--org-id", default=env("PANGOLIN_ORG_ID", DEFAULT_ORG_ID))
+ site_id_default = env("PANGOLIN_SITE_ID", str(DEFAULT_SITE_ID)) or str(DEFAULT_SITE_ID)
+ parser.add_argument("--site-id", type=int, default=int(site_id_default))
+ parser.add_argument("--domain-id", default=env("PANGOLIN_DOMAIN_ID", DEFAULT_DOMAIN_ID))
+ parser.add_argument("--name", default=DEFAULT_NAME)
+ parser.add_argument("--subdomain", default=DEFAULT_SUBDOMAIN)
+ parser.add_argument("--target-ip", default=DEFAULT_TARGET_IP)
+ parser.add_argument("--target-port", type=int, default=DEFAULT_TARGET_PORT)
+ parser.add_argument("--hc-path", default="/")
+ parser.add_argument("--dry-run", action="store_true")
+ args = parser.parse_args()
+
+ if not args.api_key:
+ return fail("Missing PANGOLIN_API_KEY / --api-key")
+
+ full_domain = f"{args.subdomain}.paccoco.com"
+
+ status, resources_resp = api_request(
+ args.api_url,
+ args.api_key,
+ "GET",
+ f"/org/{parse.quote(args.org_id)}/resources?pageSize=200",
+ )
+ if status != 200:
+ return fail("Failed to list Pangolin resources", resources_resp)
+
+ resources = resources_resp.get("data", {}).get("resources", [])
+ resource = find_resource(resources, name=args.name, full_domain=full_domain)
+
+ create_body = {
+ "name": args.name,
+ "subdomain": args.subdomain,
+ "http": True,
+ "protocol": "tcp",
+ "domainId": args.domain_id,
+ }
+
+ if resource is None:
+ if args.dry_run:
+ print(json.dumps({"action": "create_resource", "body": create_body}, indent=2))
+ resource_id = None
+ else:
+ status, create_resp = api_request(
+ args.api_url,
+ args.api_key,
+ "PUT",
+ f"/org/{parse.quote(args.org_id)}/resource",
+ create_body,
+ )
+ if status not in (200, 201):
+ return fail("Failed to create Pangolin resource", create_resp)
+ resource_id = create_resp.get("data", {}).get("resourceId") or create_resp.get("data", {}).get("resource", {}).get("resourceId")
+ if not resource_id:
+ return fail("Create resource succeeded but resourceId was missing", create_resp)
+ status, resource_resp = api_request(args.api_url, args.api_key, "GET", f"/resource/{resource_id}")
+ if status != 200:
+ return fail("Failed to read back created resource", resource_resp)
+ resource = resource_resp.get("data", {}).get("resource") or resource_resp.get("data") or {}
+ else:
+ resource_id = resource["resourceId"]
+
+ resource_update = {
+ "name": args.name,
+ "niceId": resource.get("niceId") if resource else None,
+ "subdomain": args.subdomain,
+ "ssl": True,
+ "sso": False,
+ "blockAccess": False,
+ "emailWhitelistEnabled": False,
+ "applyRules": False,
+ "domainId": args.domain_id,
+ "enabled": True,
+ "stickySession": False,
+ "tlsServerName": None,
+ "setHostHeader": None,
+ "skipToIdpId": None,
+ "headers": None,
+ "maintenanceModeEnabled": False,
+ "maintenanceModeType": "forced",
+ "maintenanceTitle": None,
+ "maintenanceMessage": None,
+ "maintenanceEstimatedTime": None,
+ "postAuthPath": None,
+ }
+
+ target_body = {
+ "siteId": args.site_id,
+ "ip": args.target_ip,
+ "port": args.target_port,
+ "enabled": True,
+ "hcEnabled": True,
+ "hcPath": args.hc_path,
+ "hcScheme": "http",
+ "hcMode": "http",
+ "hcHostname": args.target_ip,
+ "hcPort": args.target_port,
+ "hcInterval": 30,
+ "hcUnhealthyInterval": 30,
+ "hcTimeout": 10,
+ "hcHeaders": None,
+ "hcFollowRedirects": True,
+ "hcMethod": "GET",
+ "hcStatus": 200,
+ "hcTlsServerName": None,
+ "hcHealthyThreshold": 1,
+ "hcUnhealthyThreshold": 3,
+ "path": None,
+ "pathMatchType": None,
+ "rewritePath": None,
+ "rewritePathType": None,
+ }
+
+ existing_targets = resource.get("targets", []) if resource else []
+ matching_target = None
+ for target in existing_targets:
+ if target.get("siteId") == args.site_id:
+ matching_target = target
+ break
+
+ if args.dry_run:
+ print(json.dumps({
+ "resource_id": resource_id,
+ "full_domain": full_domain,
+ "resource_update": resource_update,
+ "target_action": "update" if matching_target else "create",
+ "target_id": matching_target.get("targetId") if matching_target else None,
+ "target_body": target_body,
+ }, indent=2))
+ return 0
+
+ if resource_id is not None:
+ status, update_resp = api_request(args.api_url, args.api_key, "POST", f"/resource/{resource_id}", resource_update)
+ if status not in (200, 201):
+ return fail("Failed to update Pangolin resource", update_resp)
+
+ if matching_target:
+ target_id = matching_target["targetId"]
+ status, target_resp = api_request(args.api_url, args.api_key, "POST", f"/target/{target_id}", target_body)
+ if status not in (200, 201):
+ return fail("Failed to update Pangolin target", target_resp)
+ else:
+ status, target_resp = api_request(args.api_url, args.api_key, "PUT", f"/resource/{resource_id}/target", target_body)
+ if status not in (200, 201):
+ return fail("Failed to create Pangolin target", target_resp)
+
+ status, final_resp = api_request(args.api_url, args.api_key, "GET", f"/resource/{resource_id}")
+ if status != 200:
+ return fail("Failed to read back final Pangolin resource", final_resp)
+ payload = final_resp.get("data", {}).get("resource") or final_resp.get("data") or final_resp
+ print(json.dumps(payload, indent=2))
+ return 0
+
+ return fail("Unexpected state: resource_id is missing")
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/dispatcharr/README.md b/dispatcharr/README.md
index a65b594..1d7d978 100644
--- a/dispatcharr/README.md
+++ b/dispatcharr/README.md
@@ -15,9 +15,11 @@ sudo -n docker compose --env-file .env up -d
Access:
- Direct LAN URL: `http://10.5.30.6:9191`
+- Intended public URL: `https://tv.paccoco.com` via Pangolin with Pangolin auth disabled
Notes:
- Uses Dispatcharr's recommended all-in-one container (`DISPATCHARR_ENV=aio`).
- Data is persisted at `/mnt/docker-ssd/docker/appdata/dispatcharr`.
- First-run setup creates the initial local Dispatcharr user in the web UI.
- IPTV credentials and EPG sources are added later through the UI.
+- Pangolin automation helper: `automation/bin/pangolin_upsert_dispatcharr.py` (requires Pangolin integration API key).
diff --git a/docs/architecture/network-policy-lanes-2026-05-23.html b/docs/architecture/network-policy-lanes-2026-05-23.html
new file mode 100644
index 0000000..2bac1f0
--- /dev/null
+++ b/docs/architecture/network-policy-lanes-2026-05-23.html
@@ -0,0 +1,358 @@
+
+
+
+
+
+ Paccoco VLAN Policy Lanes — 2026-05-23
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Control plane and zone definitions
+
+
+
+ Gateway / UDM Pro
+ Mgmt IP 10.5.0.1 • control surface lives only in Management
+
+
+
+ Internal Zone
+ Management + Trusted + Servers can initiate where needed
+
+
+
+ Untrusted Zone
+ IoT + Camera + Old IoT get DNS help but no broad gateway reach
+
+
+
+ Shared DNS Anchor
+ Pi-hole HA VIP 10.5.30.53 advertised to restricted lanes by DHCP
+
+
+ Internal lanes
+
+
+ Restricted / edge lanes
+
+
+ policy edge
+
+
+
+ Management
+ 10.5.0.0/24
+ control plane
+ Lives here
+ UDM Pro 10.5.0.1
+ switches, AP management
+ Operator-only admin surface
+
+
+ Trusted
+ 10.5.1.0/24 • VLAN 51
+ people + daily drivers
+ Purpose
+ phones, laptops, tablets
+ normal admin origin points
+ Can initiate toward Servers and Untrusted
+
+
+ Servers
+ 10.5.30.0/24 • VLAN 30
+ core services
+ Anchor services
+ PD .6 • Serenity .5
+ Nomad .7 • Rocinante .112
+ Pi-hole VIP 10.5.30.53
+ Service destination for the rest of the network
+
+
+
+ Internal policy summary
+
+
+ Trusted/admin workflows can reach Servers
+
+
+ Internal can initiate into restricted lanes when needed
+
+
+ Servers host shared DNS target used by restricted lanes
+
+ • Management remains the only rightful home of gateway administration.
+ • Trusted is where John/admin clients live and where most deliberate control starts.
+ • Servers is the service center, not a client junk drawer.
+
+
+
+ IoT
+ 10.5.10.0/24 • VLAN 510
+ appliances + smart-home gear
+ Kept
+ DNS -> 10.5.30.53
+ DHCP preserved
+ mDNS preserved where needed
+ No broad gateway reach
+
+
+ Camera
+ 10.5.20.0/24 • VLAN 520
+ Protect chimes + camera-adjacent gear
+ Kept
+ DNS -> 10.5.30.53
+ DHCP preserved
+ same shield posture as IoT
+ No broad gateway reach
+
+
+ Old IoT / Legacy
+ 192.168.1.0/24 • VLAN 2
+ shrinking-only containment lane
+ Still tolerated
+ DNS -> 10.5.30.53
+ same shield posture as other untrusted lanes
+ no new joins if avoidable
+ Exception-only future
+
+
+ Guest / Hotspot
+ 10.5.90.0/24 • VLAN 590
+ internet-only lane • kept separate from the internal story
+
+
+
+ Firewall intent
+
+
+ Restricted lanes get DNS service path
+
+
+ Blocked: Untrusted -> Gateway admin / broad gateway access
+
+
+ Only proven exceptions survive
+
+
+
+ Legend
+
+
+ trusted / operator lane
+
+
+ servers / service core
+
+
+ restricted edge lane
+
+
+ management / sensitive control surface
+
+
+ allowed path
+
+
+ blocked path
+
+
+ narrow preserved exception
+
+
+
+
+
+
Read this diagram as policy, not inventory
+
+ • Left side is where control and deliberate administration belong.
+ • Right side is where devices live when they need containment more than trust.
+ • The important service export from Servers is shared DNS at 10.5.30.53.
+
+
+
+
The two rules that matter most
+
+ • Internal can initiate outward when needed.
+ • Untrusted does not get broad gateway access back in, especially not UDM admin TCP.
+ • Any exception has to earn its keep with a real failing device.
+
+
+
+
What still deserves live validation
+
+ • Renew a DHCP lease on one client from each restricted lane.
+ • Confirm that client resolves through 10.5.30.53.
+ • Confirm gateway UI/admin ports still fail from those lanes.
+
+
+
+
+
Open locally with: xdg-open /home/fizzlepoof/repos/truenas-stacks/docs/architecture/network-policy-lanes-2026-05-23.html
+
+
+
+
diff --git a/docs/architecture/network-topology-2026-05-23.html b/docs/architecture/network-topology-2026-05-23.html
new file mode 100644
index 0000000..bedac42
--- /dev/null
+++ b/docs/architecture/network-topology-2026-05-23.html
@@ -0,0 +1,424 @@
+
+
+
+
+
+ Paccoco Homelab Network Topology — 2026-05-23
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ External access + WAN edge
+
+
+ UniFi control plane + switching fabric
+
+
+ Segmentation lanes
+
+
+
+
+ Internet / Clients
+ trusted users • phones • browsers
+
+
+
+ Pangolin VPS + Newt
+ reverse proxy for public subdomains
+
+
+
+ Tailscale
+ Serenity: 100.94.87.79
+
+
+
+ Cloudflare DNS
+ paccoco.com records point to Pangolin VPS
+
+
+
+
+ UDM Pro
+ gateway + UniFi controller
+ Mgmt IP: 10.5.0.1
+ Admin TCP blocked from Untrusted
+
+
+
+ Switch Fabric
+ USW-24-PoE — 10.5.0.10
+ USW Pro HD 24 — 10.5.0.135
+ 10G links for PD + Serenity • 2.5G lanes for Nomad/FlyingDutchman/Rocinante
+
+
+
+ Wi-Fi Access Points
+ U7 Pro — 10.5.0.21 — active
+ U6 LR — 10.5.0.20
+ currently disconnected in UniFi
+
+
+
+ Policy Zones
+ Internal = Mgmt + Trusted + Servers
+ Untrusted = IoT + Camera + Old IoT
+ Hotspot = Guest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Management
+ 10.5.0.0/24
+ UDM Pro • switches • AP management
+
+
+
+ Core Infra
+ UDM Pro 10.5.0.1
+ USW-24-PoE 10.5.0.10 • USW Pro HD 10.5.0.135
+
+
+
+ AP Control
+ U7 Pro active
+ U6 LR disconnected but still modeled
+
+ Management stays operator-only
+
+
+
+ Trusted
+ 10.5.1.0/24 • VLAN 51
+ laptops • phones • tablets • daily-driver clients
+
+
+
+ User Devices
+ operator/admin endpoints live here
+ can initiate into servers + untrusted lanes
+
+
+
+ Wi-Fi SSID
+ primary trusted network
+ legacy compatibility cleanup still ongoing
+
+ Allowed to initiate toward Untrusted
+
+
+
+ Servers
+ 10.5.30.0/24 • VLAN 30
+ core services + app stacks + AI + storage-adjacent hosts
+
+
+
+ Core Hosts
+ PlausibleDeniability — 10.5.30.6
+ Serenity — 10.5.30.5
+ N.O.M.A.D. — 10.5.30.7
+ Rocinante — 10.5.30.112
+
+
+
+ Shared Services
+ Pi-hole HA VIP / DNS: 10.5.30.53
+ Gitea • Home Assistant • n8n • OpenWebUI • Qdrant
+ LiteLLM • monitoring • media • productivity stacks
+
+ Internal services reachable by trusted/admin workflows
+
+
+
+ IoT
+ 10.5.10.0/24 • VLAN 510
+ ecobees • MyQ • fridge • smart appliances
+
+
+
+ DHCP DNS
+ hands out 10.5.30.53
+ no longer depends on gateway-default DNS
+
+
+
+ Gateway Access
+ general gateway access blocked
+ DHCP + mDNS preserved
+ UDM admin TCP explicitly blocked
+
+
+
+ Camera
+ 10.5.20.0/24 • VLAN 520
+ Protect chimes + camera-adjacent gear
+ Same shield posture as IoT
+ DNS -> 10.5.30.53 • DHCP/mDNS preserved
+ general gateway access blocked
+
+
+
+ Guest / Hotspot
+ 10.5.90.0/24 • VLAN 590 • internet-only lane
+
+
+
+ Old IoT / Legacy Quarantine
+ 192.168.1.0/24 • VLAN 2
+ shrinking-only containment lane
+ same DNS/gateway shield pattern as IoT/Camera
+
+
+
+ Allow Internal → Untrusted
+
+
+ DNS only → 10.5.30.53
+
+
+ Blocked: general Untrusted → Gateway
+
+
+ Blocked: UDM admin TCP from Camera / IoT / Old IoT
+
+
+ Only proven exceptions stay
+
+
+
+ Legend
+
+
+ Trusted / operator lanes
+
+
+ Servers / core services
+
+
+ IoT / camera lanes
+
+
+ Management / blocked security surfaces
+
+
+ allowed path
+
+
+ blocked path
+
+
+ narrow preserved exception
+
+
+
+
+
+
+
+ • PD 10.5.30.6 is the main Docker/control host.
+ • Serenity 10.5.30.5 handles NAS + ARR + Tailscale reachability.
+ • N.O.M.A.D. 10.5.30.7 runs local services and game/server workloads.
+ • Rocinante 10.5.30.112 carries heavy Ollama inference.
+ • UDM Pro stays at 10.5.0.1 on the management subnet.
+
+
+
+
+
+
+ • Servers VLAN is live at 10.5.30.0/24 (VLAN 30).
+ • IoT, Camera, and Old IoT now DHCP-advertise 10.5.30.53 for DNS.
+ • Broader Untrusted → Gateway block is live.
+ • DHCP and mDNS are still preserved for restricted lanes.
+ • UDM Pro admin TCP ports are explicitly blocked from Untrusted.
+
+
+
+
+
+
+ • Renew one DHCP lease per restricted lane.
+ • Confirm DNS resolution works through 10.5.30.53.
+ • Confirm gateway admin/UI access fails from those lanes.
+ • Keep Google/cast validation as a separate laptop-present test wave.
+ • Treat any new exception as earned only by a real failing device.
+
+
+
+
+
Open locally with: xdg-open /home/fizzlepoof/repos/truenas-stacks/docs/architecture/network-topology-2026-05-23.html
+
+
+
+
diff --git a/n8n-workflows/01-grafana-alert-ai-gotify-v1.4.json b/n8n-workflows/01-grafana-alert-ai-gotify-v1.4.json
index f6bdedb..a9669f8 100644
--- a/n8n-workflows/01-grafana-alert-ai-gotify-v1.4.json
+++ b/n8n-workflows/01-grafana-alert-ai-gotify-v1.4.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-09T19:58:19.095Z",
"id": "fmywVBIanfxOfDlI",
"name": "Grafana Alert → AI → Gotify v1.4",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/02-rag-ingest-and-query-v1.0.json b/n8n-workflows/02-rag-ingest-and-query-v1.0.json
index 3284abf..f0047ef 100644
--- a/n8n-workflows/02-rag-ingest-and-query-v1.0.json
+++ b/n8n-workflows/02-rag-ingest-and-query-v1.0.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-06T23:25:29.005Z",
"id": "gYyOggC4J98keLbj",
"name": "RAG Pipeline v1.0",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/02-rag-ingest-and-query.json b/n8n-workflows/02-rag-ingest-and-query.json
index 32cf035..5812b1e 100644
--- a/n8n-workflows/02-rag-ingest-and-query.json
+++ b/n8n-workflows/02-rag-ingest-and-query.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-06T23:25:29.005Z",
"id": "gYyOggC4J98keLbj",
"name": "RAG Pipeline — Ingest & Query",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/03-paperless-ai-processing-v1.0.json b/n8n-workflows/03-paperless-ai-processing-v1.0.json
index 3faefbd..27855f3 100644
--- a/n8n-workflows/03-paperless-ai-processing-v1.0.json
+++ b/n8n-workflows/03-paperless-ai-processing-v1.0.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T20:24:40.153Z",
"id": "dQ1d5deV4mF0s5eP",
"name": "Paperless AI Processing v1.0",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/03-paperless-ai-processing.json b/n8n-workflows/03-paperless-ai-processing.json
index 3faefbd..27855f3 100644
--- a/n8n-workflows/03-paperless-ai-processing.json
+++ b/n8n-workflows/03-paperless-ai-processing.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T20:24:40.153Z",
"id": "dQ1d5deV4mF0s5eP",
"name": "Paperless AI Processing v1.0",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/04-whisper-transcription.json b/n8n-workflows/04-whisper-transcription.json
index 302c233..f84377c 100644
--- a/n8n-workflows/04-whisper-transcription.json
+++ b/n8n-workflows/04-whisper-transcription.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-09T22:31:02.480Z",
"id": "9qfbZJJmSJqmg5sX",
"name": "Whisper Audio Transcription",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/05-paperless-to-rag-v1.0.json b/n8n-workflows/05-paperless-to-rag-v1.0.json
index 1f301ae..1197bd0 100644
--- a/n8n-workflows/05-paperless-to-rag-v1.0.json
+++ b/n8n-workflows/05-paperless-to-rag-v1.0.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T00:48:49.355Z",
"id": "4YKnRQ9rOiQCeYEU",
"name": "Paperless \u2192 RAG v1.0",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/05-paperless-to-rag.json b/n8n-workflows/05-paperless-to-rag.json
index 7c0b18b..5080912 100644
--- a/n8n-workflows/05-paperless-to-rag.json
+++ b/n8n-workflows/05-paperless-to-rag.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T00:48:49.355Z",
"id": "4YKnRQ9rOiQCeYEU",
"name": "Paperless \u2192 RAG Ingest",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/07-git-commit-summarizer-v1.0.json b/n8n-workflows/07-git-commit-summarizer-v1.0.json
index 6bfc486..2062704 100644
--- a/n8n-workflows/07-git-commit-summarizer-v1.0.json
+++ b/n8n-workflows/07-git-commit-summarizer-v1.0.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T00:49:43.226Z",
"id": "CFYWbBRx9RPf4HjS",
"name": "Git Commit → AI Summary → Gotify v1.0",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/07-git-commit-summarizer.json b/n8n-workflows/07-git-commit-summarizer.json
index fa2a359..5e9d87f 100644
--- a/n8n-workflows/07-git-commit-summarizer.json
+++ b/n8n-workflows/07-git-commit-summarizer.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T00:49:43.226Z",
"id": "CFYWbBRx9RPf4HjS",
"name": "Git Commit \u2192 AI Summary \u2192 Gotify",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/08-class-recording-rag.json b/n8n-workflows/08-class-recording-rag.json
index 9103405..644d6b1 100644
--- a/n8n-workflows/08-class-recording-rag.json
+++ b/n8n-workflows/08-class-recording-rag.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-10T02:04:29.809Z",
"id": "g9JRtBA5lR0OAwVo",
"name": "Class Recording → Transcribe → RAG",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/09-class-transcript-ingest-v1.1.json b/n8n-workflows/09-class-transcript-ingest-v1.1.json
index dcc1a10..8cff061 100644
--- a/n8n-workflows/09-class-transcript-ingest-v1.1.json
+++ b/n8n-workflows/09-class-transcript-ingest-v1.1.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-10T02:04:47.115Z",
"id": "sg7nRrhim0omh8oQ",
"name": "Class Transcript Ingest v1.1",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/17-paperless-intake-triage.json b/n8n-workflows/17-paperless-intake-triage.json
index 591823a..5deb6c3 100644
--- a/n8n-workflows/17-paperless-intake-triage.json
+++ b/n8n-workflows/17-paperless-intake-triage.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-13T16:06:39.571Z",
"id": "FIhrfDfL1NgakDwl",
"name": "Paperless Intake Triage → Doris Review Queue",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{
diff --git a/n8n-workflows/19-paperless-school-metadata-enrichment.json b/n8n-workflows/19-paperless-school-metadata-enrichment.json
index 243537e..caefbc8 100644
--- a/n8n-workflows/19-paperless-school-metadata-enrichment.json
+++ b/n8n-workflows/19-paperless-school-metadata-enrichment.json
@@ -3,8 +3,8 @@
"createdAt": "2026-05-13T21:26:54.606Z",
"id": "7MlM3DjAMf4eSl0s",
"name": "Paperless School Intake Metadata Enrichment v1.1",
- "description": null,
- "active": true,
+ "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
+ "active": false,
"isArchived": false,
"nodes": [
{