{
 "name": "Aufgaben-Eskalation mit Timeout",
 "nodes": [
  {
   "parameters": {
    "rule": {
     "interval": [
      {
       "field": "cronExpression",
       "expression": "0 5 * * *"
      }
     ]
    }
   },
   "id": "f6082191-b371-4755-9a2a-92e97cb00f38",
   "name": "Schedule 05:00 UTC",
   "type": "n8n-nodes-base.scheduleTrigger",
   "typeVersion": 1.2,
   "position": [
    0,
    -110
   ],
   "notes": "Läuft NACH dem pg_cron-Nachtlauf (02:15 UTC, run_nightly_signals). Zeitzone des Workflows: UTC.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "httpMethod": "POST",
    "path": "eskalation/start",
    "authentication": "headerAuth",
    "responseMode": "responseNode",
    "options": {}
   },
   "id": "11b72b3e-bdae-4a4a-b4d4-fc8e800401b9",
   "name": "Manueller Start (Webhook)",
   "type": "n8n-nodes-base.webhook",
   "typeVersion": 2,
   "position": [
    0,
    110
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Eskalation Webhook-Token"
    }
   },
   "webhookId": "32ddd3d4-bdbb-415c-8693-e116b7fdf282",
   "notes": "Demo-Start auf Knopfdruck. Geschützt per Header x-eskalation-token (Credential).",
   "notesInFlow": true
  },
  {
   "parameters": {
    "assignments": {
     "assignments": [
      {
       "id": "9697d4be-d15d-4ebb-b157-86b2ac7e0573",
       "name": "supabase_url",
       "value": "https://<projekt-ref>.supabase.co",
       "type": "string"
      },
      {
       "id": "69b456a4-29e6-4ba5-b3f2-9b0698000373",
       "name": "app_url",
       "value": "https://dental-intelligence-os.vercel.app",
       "type": "string"
      },
      {
       "id": "efc77d7d-de89-471a-8dd5-4978be2f7543",
       "name": "absender",
       "value": "eskalation@<absender-domain>",
       "type": "string"
      },
      {
       "id": "c97341f7-cf1d-4666-80c0-b97b32f73860",
       "name": "run_id",
       "value": "={{ $execution.id }}",
       "type": "string"
      },
      {
       "id": "a5e621f1-24db-47cc-8d7b-51c94c4cfa15",
       "name": "heute",
       "value": "={{ $now.toUTC().toFormat('yyyy-MM-dd') }}",
       "type": "string"
      },
      {
       "id": "dc701e32-f10d-4af2-b366-e736ab8da918",
       "name": "resume_url",
       "value": "={{ $execution.resumeUrl }}",
       "type": "string"
      }
     ]
    },
    "options": {}
   },
   "id": "d73f3068-ce50-4603-92de-2ceea338cd43",
   "name": "Konfiguration",
   "type": "n8n-nodes-base.set",
   "typeVersion": 3.4,
   "position": [
    780,
    0
   ],
   "notes": "Keine Geheimnisse hier. run_id = n8n-Execution-ID (Execution-Tagging).",
   "notesInFlow": true
  },
  {
   "parameters": {
    "method": "POST",
    "url": "={{ $json.supabase_url }}/auth/v1/token?grant_type=password",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpCustomAuth",
    "options": {},
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "{\"gotrue_meta_security\":{}}"
   },
   "id": "407af3d0-afc3-4584-acda-bf1ad9ce42eb",
   "name": "Anmeldung (JWT)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1040,
    0
   ],
   "credentials": {
    "httpCustomAuth": {
     "name": "Supabase Automationskonto (Login)"
    }
   },
   "executeOnce": true,
   "notes": "Automationskonto = Praxismitglied (management). E-Mail/Passwort kommen aus dem Custom-Auth-Credential, nie aus dem JSON. Fehler → Fehlerpfad, kein Teilzustand.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "assignments": {
     "assignments": [
      {
       "id": "4589d8ac-3630-4fb6-98cb-879846f91157",
       "name": "jwt",
       "value": "={{ $json.access_token }}",
       "type": "string"
      },
      {
       "id": "1d389aa2-d1bd-404b-a264-2d8f568e2782",
       "name": "supabase_url",
       "value": "={{ $('Konfiguration').first().json.supabase_url }}",
       "type": "string"
      },
      {
       "id": "5edf401a-80bd-47d1-8574-cee258b67f81",
       "name": "app_url",
       "value": "={{ $('Konfiguration').first().json.app_url }}",
       "type": "string"
      },
      {
       "id": "36469e1a-5d85-4af3-86a2-5c58c5c2fc51",
       "name": "absender",
       "value": "={{ $('Konfiguration').first().json.absender }}",
       "type": "string"
      },
      {
       "id": "4b62fd23-61b8-4791-a7b5-5365a4bac531",
       "name": "run_id",
       "value": "={{ $('Konfiguration').first().json.run_id }}",
       "type": "string"
      },
      {
       "id": "81e89f4b-024d-4efb-94d9-a714f50f8d81",
       "name": "heute",
       "value": "={{ $('Konfiguration').first().json.heute }}",
       "type": "string"
      },
      {
       "id": "8fb82bb7-af06-40ca-8c46-a44320916b19",
       "name": "resume_url",
       "value": "={{ $('Konfiguration').first().json.resume_url }}",
       "type": "string"
      }
     ]
    },
    "options": {}
   },
   "id": "e7e9e46c-9928-41f1-849f-67341e66718e",
   "name": "JWT ablegen",
   "type": "n8n-nodes-base.set",
   "typeVersion": 3.4,
   "position": [
    1300,
    0
   ]
  },
  {
   "parameters": {
    "respondWith": "json",
    "responseBody": "={{ JSON.stringify({ gestartet: true, execution: $execution.id }) }}",
    "options": {
     "responseCode": 202
    }
   },
   "id": "bf13ab2e-f3f5-4517-acc6-9bd3c5876d0e",
   "name": "Lauf gestartet",
   "type": "n8n-nodes-base.respondToWebhook",
   "typeVersion": 1.1,
   "position": [
    260,
    110
   ],
   "notes": "n8n ≥ 2.10 verlangt, dass ein Webhook-Trigger mit Respond-Knoten im Workflow selbst im Modus „Respond to Webhook node“ läuft — sonst „Unused Respond to Webhook node“. Der Schedule-Pfad umgeht diesen Knoten.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "method": "GET",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/escalation_settings",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "options": {},
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $('JWT ablegen').first().json.jwt }}"
      }
     ]
    },
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "select",
       "value": "practice_id,stufe1_tage,stufe2_tage,stufe3_tage,ohne_due_on_tage,dedupe_tage,hitl_timeout_h"
      }
     ]
    }
   },
   "id": "acf69133-34dd-4185-81c2-e433a7ed2062",
   "name": "Einstellungen",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1560,
    0
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "executeOnce": true,
   "notes": "Schwellen je Praxis — Setzungen, einstellbar, nicht hart kodiert.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "method": "PATCH",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/signal_escalations",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "options": {},
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $('JWT ablegen').first().json.jwt }}"
      },
      {
       "name": "Prefer",
       "value": "return=minimal"
      }
     ]
    },
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "practice_id",
       "value": "=eq.{{ $json.practice_id }}"
      },
      {
       "name": "stufe",
       "value": "eq.3"
      },
      {
       "name": "antwort",
       "value": "is.null"
      },
      {
       "name": "gesendet_am",
       "value": "=lt.{{ $now.minus({ hours: $json.hitl_timeout_h }).toISO() }}"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ antwort: \"unbeantwortet\", antwort_am: $now.toISO() }) }}"
   },
   "id": "b451c94e-3ea1-410b-add9-f407832f6b14",
   "name": "Verwaiste Entscheidungen schließen",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1820,
    0
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "alwaysOutputData": true,
   "notes": "Selbstheilung: Stufe-3-Anfragen eines früheren Laufs, deren Frist ohne Antwort verstrich (z. B. n8n-Neustart), werden als unbeantwortet geschlossen.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "method": "GET",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/practice_contact_mailboxes",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "options": {},
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $('JWT ablegen').first().json.jwt }}"
      }
     ]
    },
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "select",
       "value": "practice_id,rolle,email,practices(name)"
      }
     ]
    }
   },
   "id": "26bd378b-96c3-4ffa-9d91-4f215fcf0387",
   "name": "Bereichspostfächer",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2080,
    0
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "executeOnce": true,
   "alwaysOutputData": true,
   "notes": "Rolle → Sammeladresse. Es gibt keinen Weg zu assigned_to.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "method": "GET",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/v_eskalation_kandidaten",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "options": {},
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $('JWT ablegen').first().json.jwt }}"
      }
     ]
    },
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "ohne_due_on",
       "value": "is.false"
      },
      {
       "name": "order",
       "value": "faellig_am.asc"
      },
      {
       "name": "limit",
       "value": "1000"
      },
      {
       "name": "select",
       "value": "*"
      }
     ]
    }
   },
   "id": "e22e64ab-0032-410e-b4a6-a21039cc8189",
   "name": "Kandidaten (a) fällig",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2340,
    -110
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "executeOnce": true,
   "alwaysOutputData": true,
   "notes": "(a) status=offen ∧ due_on < heute. Sicht unter RLS (security_invoker).",
   "notesInFlow": true
  },
  {
   "parameters": {
    "method": "GET",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/v_eskalation_kandidaten",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "options": {},
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $('JWT ablegen').first().json.jwt }}"
      }
     ]
    },
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "ohne_due_on",
       "value": "is.true"
      },
      {
       "name": "order",
       "value": "faellig_am.asc"
      },
      {
       "name": "limit",
       "value": "1000"
      },
      {
       "name": "select",
       "value": "*"
      }
     ]
    }
   },
   "id": "eb93eadc-4380-4709-8fe9-4e74ec087c36",
   "name": "Kandidaten (b) ohne Fälligkeit",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2340,
    110
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "executeOnce": true,
   "alwaysOutputData": true,
   "notes": "(b) status=offen ∧ due_on is null ∧ created_at < heute − ohne_due_on_tage (Ersatzfälligkeit).",
   "notesInFlow": true
  },
  {
   "parameters": {
    "mode": "append",
    "numberInputs": 2
   },
   "id": "a12dedae-8a4c-4a70-9e31-9bd650e1aa88",
   "name": "Zusammenführen",
   "type": "n8n-nodes-base.merge",
   "typeVersion": 3,
   "position": [
    2600,
    0
   ]
  },
  {
   "parameters": {
    "jsCode": "// Stufe 3: Ein Signal gilt als unberührt, wenn seit seiner (Ersatz-)Fälligkeit kein\n// audit_log-Eintrag (table_name='action_signals', record_id=id, action='UPDATE') existiert.\n// Das Flag liefert die Sicht mengenbasiert; wer die Aufgabe angefasst hat, wird nicht eskaliert.\nconst out = [];\nfor (const { json: s } of $input.all()) {\n  if (!s.signal_id) continue;                 // leerer Platzhalter (Always Output Data)\n  if (s.beruehrt_seit_faelligkeit) continue;  // berührt → keine Eskalation\n  out.push({ json: s });\n}\nreturn out;"
   },
   "id": "c2632747-01ca-4e62-8a26-72d4ae2ae61a",
   "name": "Unberührt-Prüfung",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    0,
    660
   ],
   "alwaysOutputData": true
  },
  {
   "parameters": {
    "jsCode": "// Stufe 5 (vor der Dublettenprüfung, weil diese je Stufe gilt):\n//   Stufe 1: stufe1_tage ≤ Tage < stufe2_tage ∧ Bereich zuständig → Erinnerung an den Bereich\n//   Stufe 2: Tage ≥ stufe2_tage ODER niemand zuständig          → Praxisleitung (owner)\n//   Stufe 3: Tage ≥ stufe3_tage                                   → Entscheidungsanfrage an die Praxisleitung\n// zustaendige_rolle = assigned_role, ersatzweise die Rolle der zugewiesenen Person (nie die Person selbst).\nconst out = [];\nfor (const { json: s } of $input.all()) {\n  if (!s.signal_id) continue;\n  const t = Number(s.tage_ueberfaellig);\n  let stufe = null, empf = null;\n  if (t >= s.stufe3_tage)                         { stufe = 3; empf = 'owner'; }\n  else if (t >= s.stufe2_tage || !s.zustaendige_rolle) { stufe = 2; empf = 'owner'; }\n  else if (t >= s.stufe1_tage)                    { stufe = 1; empf = s.zustaendige_rolle; }\n  if (!stufe) continue;\n  out.push({ json: { ...s, stufe, empfaenger_rolle: empf } });\n}\nreturn out;"
   },
   "id": "64dce86c-036b-40c8-b45b-26f8287baf93",
   "name": "Stufenlogik",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    260,
    660
   ],
   "alwaysOutputData": true
  },
  {
   "parameters": {
    "jsCode": "// Stufe 4: keine zweite Eskalation derselben Stufe für dasselbe Signal innerhalb dedupe_tage.\n// stufen_im_dedupe_fenster kommt aus signal_escalations (Sicht), gesendet_am ≥ now() − dedupe_tage.\nreturn $input.all().filter(({ json: s }) => s.signal_id && !(s.stufen_im_dedupe_fenster || []).includes(s.stufe));"
   },
   "id": "4cfcd7cb-4f1b-43f4-af98-2eeb20d6051f",
   "name": "Dublettenprüfung",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    520,
    660
   ],
   "alwaysOutputData": true
  },
  {
   "parameters": {
    "jsCode": "// Für jedes Stufe-3-Signal drei Entscheidungen: +14 Tage · → Verwaltung · → Prophylaxe.\n// Jeder Link wird einzeln signiert (HMAC über signal_id|stufe|run_id|aktion|rolle).\nconst run_id = $('JWT ablegen').first().json.run_id;\nconst out = [];\nfor (const { json: s } of $input.all()) {\n  if (!s.signal_id || s.stufe !== 3) continue;\n  out.push({ json: { signal_id: s.signal_id, stufe: 3, run_id, aktion: 'wiedervorlage', rolle: '' } });\n  for (const rolle of ['management', 'prophylaxis'])\n    out.push({ json: { signal_id: s.signal_id, stufe: 3, run_id, aktion: 'neu_zugewiesen', rolle } });\n}\nreturn out;"
   },
   "id": "3147610e-9dab-45d3-a42d-e22f202e735f",
   "name": "Entscheidungslinks vorbereiten",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    780,
    660
   ],
   "alwaysOutputData": true
  },
  {
   "parameters": {
    "action": "hmac",
    "type": "SHA256",
    "value": "={{ [$json.signal_id, $json.stufe, $json.run_id, $json.aktion, $json.rolle || ''].join('|') }}",
    "dataPropertyName": "sig",
    "secret": "={{ $env.ESKALATION_HMAC_SECRET }}",
    "encoding": "hex"
   },
   "id": "55febe1c-3cd9-4382-ba69-e013451cb28f",
   "name": "Links signieren (HMAC)",
   "type": "n8n-nodes-base.crypto",
   "typeVersion": 1,
   "position": [
    1040,
    660
   ],
   "notes": "Secret nur in n8n (Umgebungsvariable ESKALATION_HMAC_SECRET am n8n-Service). Kein Verwerfen-Link — das bleibt decideSignal in der Oberfläche.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "jsCode": "// Stufe 6: je Praxis × Bereich × Stufe eine Mail; darin je Signaltyp Anzahl, ältestes Fälligkeitsdatum,\n// Summe value_eur (vorgangsbezogen) und die Signal-IDs als Deep-Links hinter den Login.\n// KEINE Namen, keine Geburtsdaten, keine Personenstatistik — nur Zahlen, Typen, IDs.\nconst cfg   = $('JWT ablegen').first().json;\nconst boxes = $('Bereichspostfächer').all().map(i => i.json).filter(b => b.practice_id);\nconst links = $('Links signieren (HMAC)').all().map(i => i.json).filter(l => l.signal_id);\nconst kand  = $('Dublettenprüfung').all().map(i => i.json).filter(s => s.signal_id);\n\nconst TYP = { recall_due: 'Recall fällig', hkp_followup: 'HKP-Nachfass', attendance_risk: 'Terminrisiko',\n              manuelle_aufgabe: 'Manuelle Aufgabe', termin_vorbereitung: 'Terminvorbereitung', nachsorge: 'Nachsorge' };\nconst ROLLE = { owner: 'Praxisleitung', management: 'Verwaltung', prophylaxis: 'Prophylaxe' };\nconst eur = n => n.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' });\nconst esc = s => String(s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst mailbox = (p, r) => boxes.find(b => b.practice_id === p && b.rolle === r);\n\nconst groups = {};\nfor (const s of kand) {\n  let box = mailbox(s.practice_id, s.empfaenger_rolle);\n  let empf = s.empfaenger_rolle;\n  if (!box) { box = mailbox(s.practice_id, 'owner'); empf = 'owner'; }   // kein Bereichspostfach → Praxisleitung\n  if (!box) throw new Error(`Kein Bereichspostfach für Praxis ${s.practice_id} (weder ${s.empfaenger_rolle} noch owner)`);\n  const key = [s.practice_id, empf, s.stufe].join('|');\n  const g = groups[key] ??= { practice_id: s.practice_id, praxis: box.practices?.name ?? s.practice_id,\n                              empfaenger_rolle: empf, stufe: s.stufe, email: box.email, typen: {}, signale: [] };\n  const t = g.typen[s.signal_type] ??= { signal_type: s.signal_type, anzahl: 0, aeltestes: null, summe_eur: 0, signal_ids: [] };\n  t.anzahl++; t.summe_eur += Number(s.value_eur || 0); t.signal_ids.push(s.signal_id);\n  if (!t.aeltestes || s.faellig_am < t.aeltestes) t.aeltestes = s.faellig_am;\n  g.signale.push({ signal_id: s.signal_id, signal_type: s.signal_type, faellig_am: s.faellig_am, tage: s.tage_ueberfaellig,\n                   value_eur: s.value_eur, ohne_due_on: s.ohne_due_on,\n                   links: links.filter(l => l.signal_id === s.signal_id) });\n}\n\nfunction render(g) {\n  const deep = id => `${cfg.app_url}/heute?signal=${id}`;\n  const sep = cfg.resume_url.includes('?') ? '&' : '?';   // n8n signiert die Resume-URL selbst (?signature=…)\n  const hitl = (id, aktion, rolle, sig) =>\n    `${cfg.resume_url}${sep}aktion=${aktion}&signal=${id}&stufe=3&run=${cfg.run_id}&rolle=${rolle}&sig=${sig}`;\n  const intro = {\n    1: `Im Bereich <b>${ROLLE[g.empfaenger_rolle]}</b> sind Aufgaben seit mindestens einem Tag überfällig und seit ihrer Fälligkeit unberührt.`,\n    2: `Diese Aufgaben sind seit mindestens 7 Tagen überfällig <i>oder</i> niemandem zugeordnet — und seit ihrer Fälligkeit unberührt.`,\n    3: `Diese Aufgaben sind seit mindestens 21 Tagen überfällig und unberührt. Bitte entscheiden Sie je Aufgabe: <b>+14 Tage wiedervorlegen</b> oder <b>Bereich neu zuweisen</b>. Verwerfen ist hier bewusst nicht möglich — das geht nur in der Anwendung. Ohne Antwort innerhalb der Frist wird die Anfrage als <i>unbeantwortet</i> protokolliert; die Aufgabe bleibt unverändert.`\n  }[g.stufe];\n  let h = `<div style=\"font-family:system-ui,sans-serif;max-width:720px;color:#1b1b1b\">\n<h2 style=\"margin:0 0 4px\">Eskalation Stufe ${g.stufe} · ${esc(g.praxis)}</h2>\n<p style=\"margin:0 0 12px;color:#555\">An: ${ROLLE[g.empfaenger_rolle]} · Stand ${cfg.heute} · Lauf ${esc(cfg.run_id)}</p>\n<p>${intro}</p>\n<table style=\"border-collapse:collapse;width:100%;margin:12px 0\"><thead><tr style=\"background:#f2f2f2\">\n<th style=\"text-align:left;padding:6px;border:1px solid #ddd\">Signaltyp</th><th style=\"padding:6px;border:1px solid #ddd\">Anzahl</th>\n<th style=\"padding:6px;border:1px solid #ddd\">Älteste Fälligkeit</th><th style=\"padding:6px;border:1px solid #ddd\">Summe (vorgangsbezogen)</th></tr></thead><tbody>`;\n  for (const t of Object.values(g.typen))\n    h += `<tr><td style=\"padding:6px;border:1px solid #ddd\">${TYP[t.signal_type] || esc(t.signal_type)}</td><td style=\"text-align:center;padding:6px;border:1px solid #ddd\">${t.anzahl}</td><td style=\"text-align:center;padding:6px;border:1px solid #ddd\">${t.aeltestes}</td><td style=\"text-align:right;padding:6px;border:1px solid #ddd\">${t.summe_eur ? eur(t.summe_eur) : '–'}</td></tr>`;\n  h += `</tbody></table>`;\n  for (const t of Object.values(g.typen)) {\n    h += `<h3 style=\"margin:16px 0 6px;font-size:15px\">${TYP[t.signal_type] || esc(t.signal_type)} (${t.anzahl})</h3><ul style=\"padding-left:18px;margin:0\">`;\n    for (const s of g.signale.filter(x => x.signal_type === t.signal_type)) {\n      h += `<li style=\"margin:3px 0\"><a href=\"${deep(s.signal_id)}\">${s.signal_id}</a> · ${s.ohne_due_on ? 'ohne Fälligkeit, ersatzweise' : 'fällig'} ${s.faellig_am} (${s.tage} Tage)${s.value_eur ? ' · ' + eur(Number(s.value_eur)) : ''}`;\n      if (g.stufe === 3 && s.links.length) {\n        const l = a => s.links.find(x => x.aktion === a.aktion && (x.rolle || '') === (a.rolle || ''));\n        const w = l({aktion:'wiedervorlage', rolle:''}), m = l({aktion:'neu_zugewiesen', rolle:'management'}), p = l({aktion:'neu_zugewiesen', rolle:'prophylaxis'});\n        h += `<br><span style=\"font-size:13px\">Entscheidung: <a href=\"${hitl(s.signal_id,'wiedervorlage','',w.sig)}\">+14 Tage wiedervorlegen</a> · <a href=\"${hitl(s.signal_id,'neu_zugewiesen','management',m.sig)}\">→ Verwaltung</a> · <a href=\"${hitl(s.signal_id,'neu_zugewiesen','prophylaxis',p.sig)}\">→ Prophylaxe</a></span>`;\n      }\n      h += `</li>`;\n    }\n    h += `</ul>`;\n  }\n  h += `<p style=\"margin-top:18px;font-size:12px;color:#777\">Diese Nachricht enthält keine Patientendaten. Jeder Link führt hinter den Login. Eskaliert wird an Bereiche, nie an Personen.</p></div>`;\n  return h;\n}\n\nreturn Object.values(groups).map(g => ({ json: {\n  practice_id: g.practice_id, praxis: g.praxis, empfaenger_rolle: g.empfaenger_rolle, stufe: g.stufe, email: g.email,\n  anzahl: g.signale.length, typen: Object.values(g.typen), signal_ids: g.signale.map(s => s.signal_id),\n  betreff: `Eskalation Stufe ${g.stufe} · ${g.praxis}: ${g.signale.length} überfällige Aufgabe${g.signale.length === 1 ? '' : 'n'} (${ROLLE[g.empfaenger_rolle]})`,\n  html: render(g) } }));"
   },
   "id": "70cbf435-9059-4ec5-a576-76423ca99532",
   "name": "Verdichten",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1300,
    660
   ]
  },
  {
   "parameters": {
    "fromEmail": "={{ $('JWT ablegen').first().json.absender }}",
    "toEmail": "={{ $json.email }}",
    "subject": "={{ $json.betreff }}",
    "emailFormat": "html",
    "html": "={{ $json.html }}",
    "options": {
     "appendAttribution": false
    }
   },
   "id": "bf195579-6fb8-46d6-9920-eed8ed19a341",
   "name": "Digest senden",
   "type": "n8n-nodes-base.emailSend",
   "typeVersion": 2.1,
   "position": [
    1560,
    660
   ],
   "credentials": {
    "smtp": {
     "name": "SMTP (EU)"
    }
   },
   "onError": "continueErrorOutput",
   "notes": "Eine Mail je Praxis × Bereich × Stufe. SMTP-Fehler → Fehlerausgang → 10 min warten → ein Retry.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "resume": "timeInterval",
    "amount": 10,
    "unit": "minutes"
   },
   "id": "3df5e809-68f3-45e3-8cba-84532448db54",
   "name": "Wait 10 min",
   "type": "n8n-nodes-base.wait",
   "typeVersion": 1.1,
   "position": [
    1560,
    880
   ],
   "webhookId": "257a179e-6cac-4727-b6f1-56a32326c084"
  },
  {
   "parameters": {
    "fromEmail": "={{ $('JWT ablegen').first().json.absender }}",
    "toEmail": "={{ $json.email }}",
    "subject": "={{ $json.betreff }}",
    "emailFormat": "html",
    "html": "={{ $json.html }}",
    "options": {
     "appendAttribution": false
    }
   },
   "id": "63c6da75-a6e4-4db9-8f52-a2cc931c27be",
   "name": "Digest erneut senden",
   "type": "n8n-nodes-base.emailSend",
   "typeVersion": 2.1,
   "position": [
    1820,
    880
   ],
   "credentials": {
    "smtp": {
     "name": "SMTP (EU)"
    }
   },
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "errorType": "errorMessage",
    "errorMessage": "=SMTP-Versand auch nach Retry fehlgeschlagen: {{ $json.error?.message ?? JSON.stringify($json.error ?? $json) }} — keine Protokollzeile geschrieben."
   },
   "id": "4e2cf7d8-4dc1-43ca-809f-57823e71bcc0",
   "name": "Versand fehlgeschlagen",
   "type": "n8n-nodes-base.stopAndError",
   "typeVersion": 1,
   "position": [
    1820,
    1100
   ],
   "notes": "Fehlerpfad: Error Trigger → Mail an Betreiber. Kein Teilzustand in signal_escalations.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "mode": "append",
    "numberInputs": 2
   },
   "id": "380724e3-e050-483d-a5fb-09e3966bb1fb",
   "name": "Versand zusammenführen",
   "type": "n8n-nodes-base.merge",
   "typeVersion": 3,
   "position": [
    1820,
    660
   ]
  },
  {
   "parameters": {
    "jsCode": "// Stufe 9: eine Zeile je Signal — erst NACH erfolgreichem Versand, in EINEM Insert (atomar).\n// empfaenger_rolle ist immer eine Rolle, nie eine Person. run_id = n8n-Execution-ID.\nconst cfg = $('JWT ablegen').first().json;\n// Die Versand-Knoten geben das SMTP-Ergebnis aus, nicht den Digest — deshalb werden die Digests\n// aus „Verdichten“ gelesen. Dieser Knoten läuft nur, wenn JEDER Digest versendet wurde (sonst Fehlerpfad).\nconst rows = [];\nfor (const { json: d } of $('Verdichten').all()) {\n  if (!d.signal_ids) continue;\n  for (const signal_id of d.signal_ids)\n    rows.push({ practice_id: d.practice_id, signal_id, stufe: d.stufe, empfaenger_rolle: d.empfaenger_rolle,\n                gesendet_am: new Date().toISOString(), run_id: cfg.run_id });\n}\nreturn [{ json: { rows, anzahl: rows.length, stufe3: rows.filter(r => r.stufe === 3).length } }];"
   },
   "id": "c61fc45b-e5f8-451a-83fa-6a39cac8d5c3",
   "name": "Protokollzeilen",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2080,
    660
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/signal_escalations",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "options": {},
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $('JWT ablegen').first().json.jwt }}"
      },
      {
       "name": "Prefer",
       "value": "return=minimal"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify($json.rows) }}"
   },
   "id": "c1f564cc-fab2-4c2e-85ba-ed9b1e22bf36",
   "name": "Protokoll schreiben",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2340,
    660
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "executeOnce": true,
   "alwaysOutputData": true,
   "notes": "Bulk-Insert = eine Transaktion. RLS: nur owner/management dürfen einfügen.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "jsCode": "// Stufe 8: Gibt es Stufe-3-Anfragen in diesem Lauf? Frist = hitl_timeout_h (max über die betroffenen Praxen).\nconst rows = $('Protokollzeilen').first().json.rows;\nconst offen = rows.filter(r => r.stufe === 3).length;\nconst kand = $('Dublettenprüfung').all().map(i => i.json).filter(s => s.stufe === 3);\nconst timeout_h = kand.length ? Math.max(...kand.map(s => Number(s.hitl_timeout_h) || 48)) : 48;\nreturn [{ json: { offen, timeout_h, run_id: $('JWT ablegen').first().json.run_id } }];"
   },
   "id": "6a628231-7135-4de0-a269-80b10d82a4ce",
   "name": "HITL-Liste",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2600,
    660
   ]
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "loose",
      "version": 2
     },
     "conditions": [
      {
       "id": "0b2b13f5-422d-40aa-9569-8e0277bc369b",
       "leftValue": "={{ $json.offen > 0 }}",
       "rightValue": true,
       "operator": {
        "type": "boolean",
        "operation": "true",
        "singleValue": true
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "3159a6eb-cda7-4404-acc9-97ee4092b1be",
   "name": "Entscheidung offen?",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    2860,
    660
   ]
  },
  {
   "parameters": {
    "resume": "webhook",
    "httpMethod": "GET",
    "responseMode": "responseNode",
    "limitWaitTime": true,
    "limitType": "afterTimeInterval",
    "resumeAmount": "={{ $('HITL-Liste').first().json.timeout_h }}",
    "resumeUnit": "hours",
    "options": {}
   },
   "id": "bb853fe1-a7a0-4bcc-8f3c-b86f0dbfa660",
   "name": "Warten auf Entscheidung",
   "type": "n8n-nodes-base.wait",
   "typeVersion": 1.1,
   "position": [
    0,
    1320
   ],
   "webhookId": "e0392d28-4231-4ec8-8bcf-cc2b0d89603f",
   "notes": "Resume per signiertem Link aus der Mail; Frist aus escalation_settings.hitl_timeout_h. Nach jeder Antwort wird erneut gewartet, bis keine offene Anfrage mehr existiert oder die Frist verstreicht.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "loose",
      "version": 2
     },
     "conditions": [
      {
       "id": "9a4bdbc1-4502-4e55-acea-08c646b5c448",
       "leftValue": "={{ $json.headers !== undefined }}",
       "rightValue": true,
       "operator": {
        "type": "boolean",
        "operation": "true",
        "singleValue": true
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "954883ed-cd11-4188-9e77-a0dc8c952a15",
   "name": "Antwort oder Frist?",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    260,
    1320
   ],
   "notes": "Webhook-Resume liefert headers/query; der Frist-Ablauf reicht das Eingangs-Item durch.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "action": "hmac",
    "type": "SHA256",
    "value": "={{ [$json.query?.signal ?? '', $json.query?.stufe ?? '', $json.query?.run ?? '', $json.query?.aktion ?? '', $json.query?.rolle ?? ''].join('|') }}",
    "dataPropertyName": "sig_erwartet",
    "secret": "={{ $env.ESKALATION_HMAC_SECRET }}",
    "encoding": "hex"
   },
   "id": "dbb36223-a70f-4f63-b092-e365a3d69b36",
   "name": "Signatur berechnen",
   "type": "n8n-nodes-base.crypto",
   "typeVersion": 1,
   "position": [
    520,
    1320
   ]
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "loose",
      "version": 2
     },
     "conditions": [
      {
       "id": "794985fb-2532-45a7-9fb7-1e3515797784",
       "leftValue": "={{ $json.query?.sig !== undefined && $json.sig_erwartet === $json.query.sig && $json.query.run === $execution.id && $json.query.stufe === '3' && ['wiedervorlage','neu_zugewiesen'].includes($json.query.aktion) && (($json.query.aktion === 'wiedervorlage' && !$json.query.rolle) || ($json.query.aktion === 'neu_zugewiesen' && ['management','prophylaxis','owner'].includes($json.query.rolle))) }}",
       "rightValue": true,
       "operator": {
        "type": "boolean",
        "operation": "true",
        "singleValue": true
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "755fffaf-e9b7-42ab-98e5-3d0315da203c",
   "name": "Signatur gültig?",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    780,
    1320
   ],
   "notes": "HMAC über signal_id|stufe|run_id|aktion|rolle. Manipulierter Link → 401, keine Änderung.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "respondWith": "text",
    "responseBody": "<!doctype html><meta charset=utf-8><title>Ungültig</title><p style=\"font-family:system-ui\">Dieser Link ist ungültig (Signatur oder Lauf stimmen nicht). Es wurde nichts geändert.</p>",
    "options": {
     "responseCode": 401,
     "responseHeaders": {
      "entries": [
       {
        "name": "Content-Type",
        "value": "text/html; charset=utf-8"
       }
      ]
     }
    }
   },
   "id": "213671e8-60c0-46a1-ac1b-07fc78a12283",
   "name": "Antwort 401",
   "type": "n8n-nodes-base.respondToWebhook",
   "typeVersion": 1.1,
   "position": [
    780,
    1540
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/auth/v1/token?grant_type=password",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpCustomAuth",
    "options": {},
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "{\"gotrue_meta_security\":{}}"
   },
   "id": "caf7125a-f084-45c4-a28e-1cb0b95b8892",
   "name": "Anmeldung erneuern",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1040,
    1320
   ],
   "credentials": {
    "httpCustomAuth": {
     "name": "Supabase Automationskonto (Login)"
    }
   },
   "executeOnce": true,
   "notes": "Das JWT aus Stufe 1 ist nach bis zu 48 h Wartezeit abgelaufen.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "assignments": {
     "assignments": [
      {
       "id": "82352bee-5f68-46f3-a44b-bbc7c6cb925c",
       "name": "jwt",
       "value": "={{ $json.access_token }}",
       "type": "string"
      },
      {
       "id": "5fb8a238-6807-47c9-898d-1a28ada6e67c",
       "name": "signal_id",
       "value": "={{ $('Signatur gültig?').item.json.query.signal }}",
       "type": "string"
      },
      {
       "id": "71487ef4-cc9a-4d67-9e54-a87550bf12ac",
       "name": "aktion",
       "value": "={{ $('Signatur gültig?').item.json.query.aktion }}",
       "type": "string"
      },
      {
       "id": "c06979d8-54ae-4dab-977b-0c1a22dbc720",
       "name": "rolle",
       "value": "={{ $('Signatur gültig?').item.json.query.rolle || '' }}",
       "type": "string"
      },
      {
       "id": "d14a415f-f1c7-4833-8c71-d260807a8d0c",
       "name": "run_id",
       "value": "={{ $('Signatur gültig?').item.json.query.run }}",
       "type": "string"
      }
     ]
    },
    "options": {}
   },
   "id": "ce251951-771d-42d5-b56d-c3c420463bb1",
   "name": "Entscheidung",
   "type": "n8n-nodes-base.set",
   "typeVersion": 3.4,
   "position": [
    1300,
    1320
   ]
  },
  {
   "parameters": {
    "method": "GET",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/signal_escalations",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "select",
       "value": "id,signal_id,stufe"
      },
      {
       "name": "signal_id",
       "value": "=eq.{{ $json.signal_id }}"
      },
      {
       "name": "stufe",
       "value": "eq.3"
      },
      {
       "name": "run_id",
       "value": "=eq.{{ $json.run_id }}"
      },
      {
       "name": "antwort",
       "value": "is.null"
      }
     ]
    },
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $json.jwt }}"
      }
     ]
    },
    "options": {}
   },
   "id": "2676bfbc-a2f7-442d-8237-3727bb0b2fb1",
   "name": "Offene Eskalation laden",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1560,
    1320
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "alwaysOutputData": true,
   "executeOnce": true,
   "notes": "Nur eine noch unbeantwortete Stufe-3-Anfrage DIESES Laufs darf eine Änderung auslösen (kein Replay).",
   "notesInFlow": true
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "loose",
      "version": 2
     },
     "conditions": [
      {
       "id": "fbf85b98-5651-4f70-afbd-d2ffec4c912a",
       "leftValue": "={{ $json.id !== undefined }}",
       "rightValue": true,
       "operator": {
        "type": "boolean",
        "operation": "true",
        "singleValue": true
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "e7e9052b-e785-4510-99da-718b9b246885",
   "name": "Eskalation offen?",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    1820,
    1320
   ]
  },
  {
   "parameters": {
    "respondWith": "text",
    "responseBody": "<!doctype html><meta charset=utf-8><title>Bereits entschieden</title><p style=\"font-family:system-ui\">Zu dieser Aufgabe liegt in diesem Lauf keine offene Anfrage mehr vor (bereits beantwortet oder Frist verstrichen). Es wurde nichts geändert.</p>",
    "options": {
     "responseCode": 409,
     "responseHeaders": {
      "entries": [
       {
        "name": "Content-Type",
        "value": "text/html; charset=utf-8"
       }
      ]
     }
    }
   },
   "id": "c27404a4-2897-43e9-a2da-47da472da645",
   "name": "Antwort 409",
   "type": "n8n-nodes-base.respondToWebhook",
   "typeVersion": 1.1,
   "position": [
    1820,
    1540
   ]
  },
  {
   "parameters": {
    "jsCode": "// HITL erlaubt genau zwei Änderungen: due_on (+14 Tage ab heute) ODER assigned_role.\n// Kein Status, kein Verwerfen, keine Person.\nconst e = $('Entscheidung').first().json;\nconst heute = new Date(); heute.setUTCHours(0, 0, 0, 0);\nconst plus14 = new Date(heute.getTime() + 14 * 86400000).toISOString().slice(0, 10);\nconst body = e.aktion === 'wiedervorlage' ? { due_on: plus14 } : { assigned_role: e.rolle };\nreturn [{ json: { ...e, body, antwort: e.aktion, antwort_am: new Date().toISOString(), neu_due_on: plus14 } }];"
   },
   "id": "38c30e51-3686-4030-91b5-e4f7697cf7bd",
   "name": "Änderung vorbereiten",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2080,
    1320
   ]
  },
  {
   "parameters": {
    "method": "PATCH",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/action_signals",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "id",
       "value": "=eq.{{ $json.signal_id }}"
      },
      {
       "name": "status",
       "value": "eq.offen"
      }
     ]
    },
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $json.jwt }}"
      },
      {
       "name": "Prefer",
       "value": "return=representation"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify($json.body) }}",
    "options": {}
   },
   "id": "edbb42e6-0895-43e6-8208-f460ab1033ce",
   "name": "Signal ändern",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2340,
    1320
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "executeOnce": true,
   "notes": "PATCH unter RLS (signals_update) als Automationskonto → Audit-Eintrag mit actor = Automationskonto.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "method": "PATCH",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/signal_escalations",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "signal_id",
       "value": "=eq.{{ $('Änderung vorbereiten').first().json.signal_id }}"
      },
      {
       "name": "stufe",
       "value": "eq.3"
      },
      {
       "name": "run_id",
       "value": "=eq.{{ $('Änderung vorbereiten').first().json.run_id }}"
      },
      {
       "name": "antwort",
       "value": "is.null"
      }
     ]
    },
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $('Änderung vorbereiten').first().json.jwt }}"
      },
      {
       "name": "Prefer",
       "value": "return=representation"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ antwort: $('Änderung vorbereiten').first().json.antwort, antwort_am: $('Änderung vorbereiten').first().json.antwort_am }) }}",
    "options": {}
   },
   "id": "67f13477-8374-4f1c-857b-4e0bbdb0411b",
   "name": "Antwort protokollieren",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2600,
    1320
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "executeOnce": true
  },
  {
   "parameters": {
    "respondWith": "text",
    "responseBody": "=<!doctype html><meta charset=utf-8><title>Entscheidung übernommen</title><p style=\"font-family:system-ui\">Entscheidung übernommen: <b>{{ $('Änderung vorbereiten').first().json.aktion === 'wiedervorlage' ? 'Wiedervorlage am ' + $('Änderung vorbereiten').first().json.neu_due_on : 'neu zugewiesen an ' + $('Änderung vorbereiten').first().json.rolle }}</b> für Aufgabe {{ $('Änderung vorbereiten').first().json.signal_id }}. Protokolliert mit Zeitstempel.</p>",
    "options": {
     "responseCode": 200,
     "responseHeaders": {
      "entries": [
       {
        "name": "Content-Type",
        "value": "text/html; charset=utf-8"
       }
      ]
     }
    }
   },
   "id": "023e48f9-5b39-4550-a520-7d52df6d2cd9",
   "name": "Bestätigung",
   "type": "n8n-nodes-base.respondToWebhook",
   "typeVersion": 1.1,
   "position": [
    2860,
    1320
   ]
  },
  {
   "parameters": {
    "method": "GET",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/signal_escalations",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "select",
       "value": "id"
      },
      {
       "name": "stufe",
       "value": "eq.3"
      },
      {
       "name": "run_id",
       "value": "=eq.{{ $('Änderung vorbereiten').first().json.run_id }}"
      },
      {
       "name": "antwort",
       "value": "is.null"
      },
      {
       "name": "limit",
       "value": "1"
      }
     ]
    },
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $('Änderung vorbereiten').first().json.jwt }}"
      }
     ]
    },
    "options": {}
   },
   "id": "79afdb89-5edd-4f1e-a212-8cfb00a49420",
   "name": "Noch offene Anfragen?",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    3120,
    1320
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "alwaysOutputData": true,
   "executeOnce": true
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "loose",
      "version": 2
     },
     "conditions": [
      {
       "id": "415dd04c-9e8c-4230-80e3-b4449d1a56be",
       "leftValue": "={{ $json.id !== undefined }}",
       "rightValue": true,
       "operator": {
        "type": "boolean",
        "operation": "true",
        "singleValue": true
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "b15b04fd-bd0d-438f-aaa8-8f05e4404616",
   "name": "Weiter warten?",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    3380,
    1320
   ]
  },
  {
   "parameters": {},
   "id": "ff87d580-2cf8-4c8a-83ca-62cf76289c94",
   "name": "Lauf beendet",
   "type": "n8n-nodes-base.noOp",
   "typeVersion": 1,
   "position": [
    3640,
    1320
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/auth/v1/token?grant_type=password",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpCustomAuth",
    "options": {},
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "{\"gotrue_meta_security\":{}}"
   },
   "id": "f6df9ac4-affc-4ec7-bac1-6bd7c9a6d948",
   "name": "Anmeldung erneuern (Frist)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    260,
    1540
   ],
   "credentials": {
    "httpCustomAuth": {
     "name": "Supabase Automationskonto (Login)"
    }
   },
   "executeOnce": true
  },
  {
   "parameters": {
    "method": "PATCH",
    "url": "={{ $('JWT ablegen').first().json.supabase_url }}/rest/v1/signal_escalations",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "run_id",
       "value": "=eq.{{ $('JWT ablegen').first().json.run_id }}"
      },
      {
       "name": "stufe",
       "value": "eq.3"
      },
      {
       "name": "antwort",
       "value": "is.null"
      }
     ]
    },
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "=Bearer {{ $json.access_token }}"
      },
      {
       "name": "Prefer",
       "value": "return=representation"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ antwort: 'unbeantwortet', antwort_am: $now.toISO() }) }}",
    "options": {}
   },
   "id": "b70c580f-881e-4a09-9504-d98d6a529330",
   "name": "Unbeantwortet protokollieren",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    520,
    1540
   ],
   "credentials": {
    "httpHeaderAuth": {
     "name": "Supabase apikey (anon)"
    }
   },
   "executeOnce": true,
   "alwaysOutputData": true,
   "notes": "Frist verstrichen: Signal unverändert, Eskalation als unbeantwortet protokolliert.",
   "notesInFlow": true
  },
  {
   "parameters": {
    "content": "## Aufgaben-Eskalation mit Timeout (Vorschlag B)\n**Leitplanken:** RLS statt Service-Rolle · Eskalation an Bereiche, nie an Personen · keine Patientendaten in der Mail · kein Verwerfen per Link (Art. 22) · E7/E9 unberührt · Schwellen sind Setzungen (escalation_settings).\n\n**Stufen:** 1 Auslöser+Anmeldung → 2 Kandidaten → 3 Unberührt → 5 Stufenlogik → 4 Dubletten → 6 Verdichten → 7 Versand (+1 Retry) → 9 Protokoll → 8 HITL (Wait + signierte Links, Frist) → 10 Fehlerpfad (eigener Workflow).",
    "height": 300,
    "width": 1500,
    "color": 4
   },
   "id": "a0bea209-1790-4de4-b910-1daf65814d84",
   "name": "Notiz 46",
   "type": "n8n-nodes-base.stickyNote",
   "typeVersion": 1,
   "position": [
    -40,
    -560
   ]
  },
  {
   "parameters": {
    "content": "### Stufe 8 · Entscheidung der Praxisleitung (HITL)\nLinks in der Stufe-3-Mail: **+14 Tage** · **→ Verwaltung** · **→ Prophylaxe**. Jeder Link: HMAC über `signal_id|stufe|run_id|aktion|rolle`. Nur eine offene Anfrage *dieses* Laufs darf ändern. Nach jeder Antwort wird weiter gewartet; Frist ohne Antwort → `unbeantwortet`, Signal unverändert.",
    "height": 220,
    "width": 1500,
    "color": 5
   },
   "id": "fc889db9-a046-44c3-b777-8392d48d3a1a",
   "name": "Notiz 47",
   "type": "n8n-nodes-base.stickyNote",
   "typeVersion": 1,
   "position": [
    -40,
    960
   ]
  }
 ],
 "connections": {
  "Schedule 05:00 UTC": {
   "main": [
    [
     {
      "node": "Konfiguration",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Manueller Start (Webhook)": {
   "main": [
    [
     {
      "node": "Lauf gestartet",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Lauf gestartet": {
   "main": [
    [
     {
      "node": "Konfiguration",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Konfiguration": {
   "main": [
    [
     {
      "node": "Anmeldung (JWT)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Anmeldung (JWT)": {
   "main": [
    [
     {
      "node": "JWT ablegen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "JWT ablegen": {
   "main": [
    [
     {
      "node": "Einstellungen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Einstellungen": {
   "main": [
    [
     {
      "node": "Verwaiste Entscheidungen schließen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Verwaiste Entscheidungen schließen": {
   "main": [
    [
     {
      "node": "Bereichspostfächer",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Bereichspostfächer": {
   "main": [
    [
     {
      "node": "Kandidaten (a) fällig",
      "type": "main",
      "index": 0
     },
     {
      "node": "Kandidaten (b) ohne Fälligkeit",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Kandidaten (a) fällig": {
   "main": [
    [
     {
      "node": "Zusammenführen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Kandidaten (b) ohne Fälligkeit": {
   "main": [
    [
     {
      "node": "Zusammenführen",
      "type": "main",
      "index": 1
     }
    ]
   ]
  },
  "Zusammenführen": {
   "main": [
    [
     {
      "node": "Unberührt-Prüfung",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Unberührt-Prüfung": {
   "main": [
    [
     {
      "node": "Stufenlogik",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Stufenlogik": {
   "main": [
    [
     {
      "node": "Dublettenprüfung",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Dublettenprüfung": {
   "main": [
    [
     {
      "node": "Entscheidungslinks vorbereiten",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Entscheidungslinks vorbereiten": {
   "main": [
    [
     {
      "node": "Links signieren (HMAC)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Links signieren (HMAC)": {
   "main": [
    [
     {
      "node": "Verdichten",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Verdichten": {
   "main": [
    [
     {
      "node": "Digest senden",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Digest senden": {
   "main": [
    [
     {
      "node": "Versand zusammenführen",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Wait 10 min",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Wait 10 min": {
   "main": [
    [
     {
      "node": "Digest erneut senden",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Digest erneut senden": {
   "main": [
    [
     {
      "node": "Versand zusammenführen",
      "type": "main",
      "index": 1
     }
    ],
    [
     {
      "node": "Versand fehlgeschlagen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Versand zusammenführen": {
   "main": [
    [
     {
      "node": "Protokollzeilen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Protokollzeilen": {
   "main": [
    [
     {
      "node": "Protokoll schreiben",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Protokoll schreiben": {
   "main": [
    [
     {
      "node": "HITL-Liste",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "HITL-Liste": {
   "main": [
    [
     {
      "node": "Entscheidung offen?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Entscheidung offen?": {
   "main": [
    [
     {
      "node": "Warten auf Entscheidung",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Lauf beendet",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Warten auf Entscheidung": {
   "main": [
    [
     {
      "node": "Antwort oder Frist?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Antwort oder Frist?": {
   "main": [
    [
     {
      "node": "Signatur berechnen",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Anmeldung erneuern (Frist)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Signatur berechnen": {
   "main": [
    [
     {
      "node": "Signatur gültig?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Signatur gültig?": {
   "main": [
    [
     {
      "node": "Anmeldung erneuern",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Antwort 401",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Antwort 401": {
   "main": [
    [
     {
      "node": "Warten auf Entscheidung",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Anmeldung erneuern": {
   "main": [
    [
     {
      "node": "Entscheidung",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Entscheidung": {
   "main": [
    [
     {
      "node": "Offene Eskalation laden",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Offene Eskalation laden": {
   "main": [
    [
     {
      "node": "Eskalation offen?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Eskalation offen?": {
   "main": [
    [
     {
      "node": "Änderung vorbereiten",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Antwort 409",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Antwort 409": {
   "main": [
    [
     {
      "node": "Warten auf Entscheidung",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Änderung vorbereiten": {
   "main": [
    [
     {
      "node": "Signal ändern",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Signal ändern": {
   "main": [
    [
     {
      "node": "Antwort protokollieren",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Antwort protokollieren": {
   "main": [
    [
     {
      "node": "Bestätigung",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Bestätigung": {
   "main": [
    [
     {
      "node": "Noch offene Anfragen?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Noch offene Anfragen?": {
   "main": [
    [
     {
      "node": "Weiter warten?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Weiter warten?": {
   "main": [
    [
     {
      "node": "Warten auf Entscheidung",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Lauf beendet",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Anmeldung erneuern (Frist)": {
   "main": [
    [
     {
      "node": "Unbeantwortet protokollieren",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Unbeantwortet protokollieren": {
   "main": [
    [
     {
      "node": "Lauf beendet",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "settings": {
  "executionOrder": "v1",
  "timezone": "UTC",
  "saveManualExecutions": true,
  "errorWorkflow": "<ID des Workflows \"Aufgaben-Eskalation — Fehlerpfad\" nach dem Import>"
 }
}