{
  "name": "Morgen-Briefing Sicherheit & Updates (alpin-code)",
  "nodes": [
    {
      "id": "t-glich-uhr",
      "name": "Täglich 6:30 Uhr",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "30 6 * * *"
            }
          ]
        }
      }
    },
    {
      "id": "jetzt-testen",
      "name": "Jetzt testen",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        200
      ],
      "parameters": {}
    },
    {
      "id": "quellen",
      "name": "Quellen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Quellen und Stichworte. Nur hier anpassen.\n// name: Überschrift in der Mail · url: RSS- oder Atom-Feed · stichworte: leer = alles Neue, sonst nur Titel mit einem davon\n// ausser: Titel mit einem dieser Wörter fallen weg (z. B. wandernde Tags wie „stable“ oder Test-Builds)\nconst QUELLEN = [\n  { name: 'Claude Code (Releases)', url: 'https://github.com/anthropics/claude-code/releases.atom', stichworte: [] },\n  { name: 'n8n (Releases)', url: 'https://github.com/n8n-io/n8n/releases.atom', stichworte: ['n8n@'], ausser: ['-exp.', '-rc', '-beta'] },\n  { name: 'Anthropic (Störungen)', url: 'https://status.anthropic.com/history.atom', stichworte: [] },\n  { name: 'BSI CERT-Bund (Warnmeldungen)', url: 'https://wid.cert-bund.de/content/public/securityAdvisory/rss', stichworte: ['[kritisch]'] },\n  { name: 'heise Security', url: 'https://www.heise.de/security/rss/news-atom.xml', stichworte: [] },\n];\nreturn QUELLEN.map((q) => ({ json: q }));"
      }
    },
    {
      "id": "feed-holen",
      "name": "Feed holen",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        100
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 20000,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "neues-auswerten",
      "name": "Neues auswerten",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Liest jede Quelle, behält nur neue Einträge und baut die Mail. Ohne KI, nur Titel, Datum und Link.\nconst quellen = $('Quellen').all().map((i) => i.json);\nconst antworten = $input.all();\nconst merker = $getWorkflowStaticData('global');\nmerker.gesehen ||= {};\nconst jetzt = Date.now();\nconst ERSTLAUF_TAGE = 3; // beim allerersten Lauf nur die letzten Tage, nicht das ganze Archiv\nconst erstlauf = Object.keys(merker.gesehen).length === 0;\n\nconst entities = (s) => String(s || '')\n  .replace(/<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>/g, '$1')\n  .replace(/<[^>]+>/g, '')\n  .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '\"').replace(/&#39;|&apos;/g, \"'\")\n  .replace(/&#(\\d+);/g, (m, n) => String.fromCharCode(Number(n))).replace(/&amp;/g, '&')\n  .replace(/\\s+/g, ' ').trim();\nconst feld = (block, namen) => {\n  for (const n of namen) {\n    const m = new RegExp('<' + n + '(?:\\\\s[^>]*)?>([\\\\s\\\\S]*?)</' + n + '>', 'i').exec(block);\n    if (m && entities(m[1])) return entities(m[1]);\n  }\n  return '';\n};\nconst linkVon = (block) => {\n  const atom = /<link\\b[^>]*?href=\"([^\"]+)\"[^>]*\\/?>/i.exec(block);\n  if (atom) return atom[1].replace(/&amp;/g, '&');\n  return feld(block, ['link']);\n};\nconst datumVon = (block) => {\n  const roh = feld(block, ['pubDate', 'published', 'updated', 'dc:date']);\n  const d = roh ? new Date(roh) : null;\n  return d && !isNaN(d) ? d : null;\n};\nconst tag = (d) => d ? new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit' }).format(d) : '';\n\nconst abschnitte = [];\nconst ausfall = [];\nlet neuGesamt = 0;\nquellen.forEach((q, i) => {\n  const a = antworten[i] ? antworten[i].json : {};\n  const text = typeof a.data === 'string' ? a.data : '';\n  if (a.error || !text || !/<(rss|feed|rdf)/i.test(text)) {\n    ausfall.push(q.name + ' (' + (a.error ? String(a.error.message || a.error).slice(0, 80) : 'keine gültige Antwort') + ')');\n    return;\n  }\n  const bloecke = text.match(/<item[\\s>][\\s\\S]*?<\\/item>|<entry[\\s>][\\s\\S]*?<\\/entry>/gi) || [];\n  const stich = (q.stichworte || []).map((s) => String(s).toLowerCase());\n  const ausser = (q.ausser || []).map((s) => String(s).toLowerCase());\n  const neu = [];\n  for (const b of bloecke) {\n    const titel = feld(b, ['title']).slice(0, 200);\n    const link = linkVon(b);\n    const id = feld(b, ['guid', 'id']) || link || titel;\n    if (!titel || !id) continue;\n    const schluessel = q.url + '|' + id;\n    if (merker.gesehen[schluessel]) continue;\n    const datum = datumVon(b);\n    merker.gesehen[schluessel] = jetzt;\n    if (erstlauf && datum && jetzt - datum.getTime() > ERSTLAUF_TAGE * 86400000) continue;\n    if (stich.length && !stich.some((s) => titel.toLowerCase().includes(s))) continue;\n    if (ausser.some((s) => titel.toLowerCase().includes(s))) continue;\n    neu.push({ titel, link, datum });\n  }\n  neuGesamt += neu.length;\n  if (neu.length) {\n    abschnitte.push(q.name + ' (' + neu.length + ')\\n' + neu.slice(0, 15).map((e) =>\n      '- ' + (e.datum ? tag(e.datum) + ' ' : '') + e.titel + (e.link ? '\\n  ' + e.link : '')).join('\\n') +\n      (neu.length > 15 ? '\\n  … und ' + (neu.length - 15) + ' weitere' : ''));\n  }\n});\n\n// Merker kürzen: Einträge älter als 45 Tage vergessen, damit die Workflow-Daten nicht wachsen.\nfor (const [k, t] of Object.entries(merker.gesehen)) if (jetzt - t > 45 * 86400000) delete merker.gesehen[k];\n\nconst heute = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date());\nconst kopf = neuGesamt ? neuGesamt + ' neue Meldung' + (neuGesamt === 1 ? '' : 'en') : 'heute nichts Neues';\nconst text = 'Morgen-Briefing ' + heute + ': ' + kopf + '\\n\\n' +\n  (abschnitte.length ? abschnitte.join('\\n\\n') : 'Seit dem letzten Briefing ist in den Quellen nichts Neues erschienen.') +\n  (ausfall.length ? '\\n\\nNicht erreichbar (bitte beim nächsten Mal prüfen):\\n' + ausfall.map((x) => '- ' + x).join('\\n') : '') +\n  '\\n\\nQuellen: ' + quellen.map((q) => q.name).join(', ') + '.\\nNur Titel und Links, ohne Zusammenfassung. Die Meldung selbst lesen, bevor gehandelt wird.';\nreturn [{ json: { betreff: 'Morgen-Briefing ' + heute + ': ' + kopf, text } }];"
      }
    },
    {
      "id": "briefing-senden",
      "name": "Briefing senden",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        880,
        100
      ],
      "parameters": {
        "fromEmail": "noreply@beispielbetrieb.de",
        "toEmail": "briefing@beispielbetrieb.de",
        "subject": "={{ $json.betreff }}",
        "emailFormat": "text",
        "text": "={{ $json.text }}",
        "options": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "smtp": {
          "id": "SMTP_ID",
          "name": "SMTP Versand"
        }
      }
    }
  ],
  "connections": {
    "Täglich 6:30 Uhr": {
      "main": [
        [
          {
            "node": "Quellen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Jetzt testen": {
      "main": [
        [
          {
            "node": "Quellen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quellen": {
      "main": [
        [
          {
            "node": "Feed holen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Feed holen": {
      "main": [
        [
          {
            "node": "Neues auswerten",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Neues auswerten": {
      "main": [
        [
          {
            "node": "Briefing senden",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "none",
    "timezone": "Europe/Berlin"
  },
  "pinData": {}
}
