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

Paccoco VLAN Policy Lanes

+
+
+ Simplified operator view focused on VLAN identity and firewall intent as of 2026-05-23. This intentionally strips most host-by-host detail so the important story is obvious: which lanes are internal, which are untrusted, where DNS now lands, and which paths are allowed, blocked, or preserved only as narrow exceptions. +
+
+
Internal zone: Management + Trusted + Servers
+
Untrusted zone: IoT + Camera + Old IoT
+
Hotspot: Guest internet-only
+
Restricted-lane DNS target: 10.5.30.53
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + 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 + + + + +
+
+
+
+

Paccoco Homelab Network Topology

+
+
+ Current-state operator map as of 2026-05-23. Reflects live UniFi segmentation, the new Servers VLAN, + restricted-lane DNS cutover to 10.5.30.53, and the broader Untrusted → Gateway shield with DHCP/mDNS preservation. +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + 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 + +
+ +
+
+
+
+

Core hosts + routing facts

+
+
    +
  • • 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.
  • +
+
+ +
+
+
+

What changed recently

+
+
    +
  • • 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.
  • +
+
+ +
+
+
+

Still worth validating on real clients

+
+
    +
  • • 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": [ {