{
 "name": "Office morning: Nextcloud briefing (daniele-michelin.com)",
 "nodes": [
  {
   "parameters": {
    "httpMethod": "POST",
    "path": "your-secret-path",
    "responseMode": "onReceived",
    "options": {}
   },
   "id": "w1",
   "name": "Besucher-Knopf (Webhook)",
   "type": "n8n-nodes-base.webhook",
   "typeVersion": 2,
   "position": [
    0,
    400
   ],
   "webhookId": "5b7c2d1e-9a43-4e1b-8c3e-1c0000000b01"
  },
  {
   "parameters": {
    "mode": "runOnceForAllItems",
    "jsCode": "// ============================================================================================================================\n//  DEMO-BEREICH (fest verdrahtet) – the ONLY places in the real Nextcloud account this workflow may read or write:\n//    • Kalender   n8n-demo                  (/remote.php/dav/calendars/YOUR-USER/n8n-demo/)\n//    • Deck-Board 1 «Büro-Morgen (Demo)»    (Stapel 1 Eingang · 2 Heute · 3 In Arbeit · 4 Warten auf · 5 Erledigt)\n//    • Ordner     /n8n-Demo/ und /n8n-Demo/Briefings/\n//    • Talk-Raum  YOUR-TALK-TOKEN «n8n-Demo · Musterbüro Team» (only messages of the demo bots are used)\n//  Everything in these spaces is fake data of the fictional «Musterbüro GmbH». Visitors of the public website trigger this\n//  workflow, so it must NEVER touch any other calendar, board, folder or conversation of the account. Every URL used by the\n//  HTTP nodes comes from here, and the guard below stops the run if one of them points anywhere else.\n//  Cleanup (keep the newest 5) only deletes items this workflow created inside these spaces (see \"Büro-Lage auswerten\").\n// ============================================================================================================================\nconst NC = 'https://YOUR-NEXTCLOUD.example';\nconst USER = 'YOUR-USER';\nconst CAL = `${NC}/remote.php/dav/calendars/${USER}/n8n-demo/`;\nconst FILES = `${NC}/remote.php/dav/files/${USER}/n8n-Demo/`;\nconst BRIEFINGS = `${FILES}Briefings/`;\nconst DECK_BOARD = 1, DECK_STACK_TODAY = 2;\nconst DECK = `${NC}/index.php/apps/deck/api/v1.0/boards/${DECK_BOARD}`;\nconst TALK = `${NC}/ocs/v2.php/apps/spreed/api/v1/chat/YOUR-TALK-TOKEN`;\n\nconst ALLOWED = [CAL, FILES, DECK + '/', TALK + '?'];\nconst ok = u => ALLOWED.some(p => String(u).startsWith(p));\n\nconst req = ($('Besucher-Knopf (Webhook)').first().json.body) || {};\nif (!/^[a-z0-9]{10}$/.test(String(req.id || ''))) throw new Error('bad id');\nconst lang = ['de', 'en', 'it'].includes(req.lang) ? req.lang : 'de';\n\n// Berlin time without libraries: offset of Europe/Berlin at a given instant\nconst TZ = 'Europe/Berlin';\nconst parts = ts => { const o = {}; for (const p of new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit',\n  hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(ts))) o[p.type] = p.value; return o; };\nconst offset = ts => { const p = parts(ts); return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second) - Math.floor(ts / 1000) * 1000; };\nconst local = (y, m, d, h, mi) => { const g = Date.UTC(y, m - 1, d, h, mi); let t = g - offset(g); t = g - offset(t); return t; };\nconst now = Date.now();\nconst p = parts(now);\nconst day0 = local(+p.year, +p.month, +p.day, 0, 0);\nconst iso = t => new Date(t).toISOString().replace(/\\.\\d{3}Z$/, 'Z');\nconst ical = t => iso(t).replace(/[-:]/g, '');\nconst ymd = t => { const q = parts(t); return `${q.year}-${q.month}-${q.day}`; };\nconst start = day0, end = local(+p.year, +p.month, +p.day + 2, 0, 0);   // today and tomorrow (Berlin)\n\nconst urls = {\n  cal_report: CAL,\n  cal_list: CAL,\n  deck_stacks: `${DECK}/stacks`,\n  deck_today: `${DECK}/stacks/${DECK_STACK_TODAY}/cards`,\n  files: FILES,\n  briefings: BRIEFINGS,\n  talk: `${TALK}?lookIntoFuture=0&limit=40&setReadMarker=0`,\n};\nfor (const [k, u] of Object.entries(urls)) if (!ok(u)) throw new Error('URL outside the demo space: ' + k);\n\nconst report = '<?xml version=\"1.0\" encoding=\"utf-8\"?>'\n  + '<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\"><d:prop><d:getetag/>'\n  + `<c:calendar-data><c:expand start=\"${ical(start)}\" end=\"${ical(end)}\"/></c:calendar-data></d:prop>`\n  + `<c:filter><c:comp-filter name=\"VCALENDAR\"><c:comp-filter name=\"VEVENT\"><c:time-range start=\"${ical(start)}\" end=\"${ical(end)}\"/>`\n  + '</c:comp-filter></c:comp-filter></c:filter></c:calendar-query>';\nconst propfind = '<?xml version=\"1.0\" encoding=\"utf-8\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:getlastmodified/><d:getcontentlength/>'\n  + '<d:getcontenttype/><d:resourcetype/></d:prop></d:propfind>';\n\nreturn [{ json: { id: req.id, lang, now: iso(now), today: ymd(now), tomorrow: ymd(end - 3600000), range: [iso(start), iso(end)],\n  urls, allowed: ALLOWED, deck_board: DECK_BOARD, deck_stack_today: DECK_STACK_TODAY, report, propfind } }];"
   },
   "id": "c0",
   "name": "Demo-Bereich festlegen",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    220,
    400
   ]
  },
  {
   "parameters": {
    "method": "REPORT",
    "url": "={{ $('Demo-Bereich festlegen').first().json.urls.cal_report }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Depth",
       "value": "1"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    },
    "sendBody": true,
    "contentType": "raw",
    "rawContentType": "application/xml",
    "body": "={{ $('Demo-Bereich festlegen').first().json.report }}"
   },
   "id": "r1",
   "name": "Kalender: Termine heute & morgen",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    480,
    0
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "method": "PROPFIND",
    "url": "={{ $('Demo-Bereich festlegen').first().json.urls.cal_list }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Depth",
       "value": "1"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    },
    "sendBody": true,
    "contentType": "raw",
    "rawContentType": "application/xml",
    "body": "={{ $('Demo-Bereich festlegen').first().json.propfind }}"
   },
   "id": "r2",
   "name": "Kalender: Einträge auflisten",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    480,
    160
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "method": "GET",
    "url": "={{ $('Demo-Bereich festlegen').first().json.urls.deck_stacks }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "OCS-APIRequest",
       "value": "true"
      },
      {
       "name": "Accept",
       "value": "application/json"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    }
   },
   "id": "r3",
   "name": "Deck: Board lesen",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    480,
    320
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "method": "PROPFIND",
    "url": "={{ $('Demo-Bereich festlegen').first().json.urls.files }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Depth",
       "value": "1"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    },
    "sendBody": true,
    "contentType": "raw",
    "rawContentType": "application/xml",
    "body": "={{ $('Demo-Bereich festlegen').first().json.propfind }}"
   },
   "id": "r4",
   "name": "Dateien: Demo-Ordner lesen",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    480,
    480
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "method": "PROPFIND",
    "url": "={{ $('Demo-Bereich festlegen').first().json.urls.briefings }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Depth",
       "value": "1"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    },
    "sendBody": true,
    "contentType": "raw",
    "rawContentType": "application/xml",
    "body": "={{ $('Demo-Bereich festlegen').first().json.propfind }}"
   },
   "id": "r5",
   "name": "Dateien: Briefings lesen",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    480,
    640
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "method": "GET",
    "url": "={{ $('Demo-Bereich festlegen').first().json.urls.talk }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "OCS-APIRequest",
       "value": "true"
      },
      {
       "name": "Accept",
       "value": "application/json"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    }
   },
   "id": "r6",
   "name": "Talk: Team-Chat lesen",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    480,
    800
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "numberInputs": 6
   },
   "id": "m1",
   "name": "Alles einsammeln",
   "type": "n8n-nodes-base.merge",
   "typeVersion": 3,
   "position": [
    760,
    400
   ]
  },
  {
   "parameters": {
    "mode": "runOnceForAllItems",
    "jsCode": "// Fake files of the \"Musterbüro GmbH\" (all names, numbers and companies are invented).\n// Used twice: by ops/ncdemo/seed.sh (first upload) and inside the n8n Code node \"Büro-Lage auswerten\", which re-uploads a seed\n// file when it is older than 20 h, so \"new in the last 24 h\" never runs dry. Pure JS, no modules (it is pasted into a Code node).\nfunction demoFiles(now) {\n  const pad = n => String(n).padStart(2, '0');\n  const d = new Date(now.getTime() - 86400000);              // \"yesterday\" in the documents\n  const de = x => pad(x.getUTCDate()) + '.' + pad(x.getUTCMonth() + 1) + '.' + x.getUTCFullYear();\n  const due = new Date(now.getTime() + 13 * 86400000);\n  // minimal one-page PDF (Helvetica, ASCII only, umlauts as octal escapes of WinAnsiEncoding)\n  const pdf = (lines) => {\n    const esc = s => s.replace(/\\\\/g, '\\\\\\\\').replace(/\\(/g, '\\\\(').replace(/\\)/g, '\\\\)')\n      .replace(/ä/g, '\\\\344').replace(/ö/g, '\\\\366').replace(/ü/g, '\\\\374').replace(/Ä/g, '\\\\304').replace(/Ö/g, '\\\\326')\n      .replace(/Ü/g, '\\\\334').replace(/ß/g, '\\\\337').replace(/€/g, '\\\\200').replace(/·/g, '\\\\267').replace(/–/g, '\\\\226').replace(/[^\\x20-\\x7e]/g, '?');\n    let y = 790, stream = 'BT\\n';\n    for (const [size, bold, text, gap] of lines) {\n      y -= gap;\n      stream += `/${bold ? 'F2' : 'F1'} ${size} Tf 1 0 0 1 56 ${y} Tm (${esc(text)}) Tj\\n`;\n    }\n    stream += 'ET\\n0.75 0.8 0.85 RG 56 700 m 539 700 l S\\n';\n    const objs = [\n      '<< /Type /Catalog /Pages 2 0 R >>',\n      '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',\n      '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R /F2 5 0 R >> >> /Contents 6 0 R >>',\n      '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>',\n      '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>',\n      `<< /Length ${stream.length} >>\\nstream\\n${stream}endstream`,\n    ];\n    let out = '%PDF-1.4\\n';\n    const off = [];\n    objs.forEach((o, i) => { off.push(out.length); out += `${i + 1} 0 obj\\n${o}\\nendobj\\n`; });\n    const x = out.length;\n    out += `xref\\n0 ${objs.length + 1}\\n0000000000 65535 f \\n` + off.map(o => String(o).padStart(10, '0') + ' 00000 n \\n').join('');\n    out += `trailer\\n<< /Size ${objs.length + 1} /Root 1 0 R >>\\nstartxref\\n${x}\\n%%EOF\\n`;\n    return out;\n  };\n  return [\n    { name: 'Rechnung_RE-2026-114_Mueller-und-Soehne.pdf', mime: 'application/pdf', content: pdf([\n      [18, true, 'Musterbüro GmbH', 0], [9, false, 'Beispielweg 12 · 20095 Hamburg · DEMO, alle Angaben erfunden', 16],\n      [15, true, 'Rechnung RE-2026-114', 60], [10, false, 'Rechnungsdatum: ' + de(d) + '   ·   fällig am ' + de(due), 18],\n      [10, false, 'Kunde: Müller & Söhne KG (fiktiv), Musterstraße 3, 22041 Hamburg', 14],\n      [11, true, 'Pos.  Leistung                                        Betrag', 34],\n      [10, false, '1     Website-Relaunch, Konzept und Design         2.400,00 €', 18],\n      [10, false, '2     Umsetzung, 6 Seiten, responsiv                  3.150,00 €', 16],\n      [10, false, '3     Schulung Redaktion (2 h)                           260,00 €', 16],\n      [10, false, '      Netto 5.810,00 €   ·   USt. 19 % 1.103,90 €', 24],\n      [12, true, '      Gesamt 6.913,90 €', 18],\n      [9, false, 'Bitte prüfen: Position 2 weicht vom Angebot (2.950,00 €) ab.', 40],\n      [8, false, 'Demo-Dokument für den n8n-Workflow \"Büro-Morgen\" auf daniele-michelin.com. Kein echtes Dokument.', 30],\n    ]) },\n    { name: 'Protokoll_Teammeeting.md', mime: 'text/markdown', content: [\n      '# Protokoll Teammeeting, ' + de(d), '', '_Musterbüro GmbH (fiktiv) · Demo-Daten_', '',\n      'Anwesend: Jonas (Vertrieb), Lea (neu, Assistenz), Murat (Projekte), Sabine (Buchhaltung)', '',\n      '## Beschlüsse', '- Angebot Bäckerei Huber geht spätestens morgen raus, Preis wie besprochen.',\n      '- Rechnung RE-2026-114 an Müller & Söhne vor dem Versand prüfen (Position 2!).',\n      '- Lea übernimmt ab sofort die Bestellungen für Büromaterial.', '',\n      '## Aufgaben', '- [ ] Jonas: Rückruf Papierwerk Nord wegen Lieferverzug',\n      '- [ ] Sabine: Unterlagen für die Umsatzsteuer-Voranmeldung an das Steuerbüro',\n      '- [ ] Murat: Feedback-Liste Relaunch bis Freitag abarbeiten', '',\n      '## Nächstes Treffen', 'Montag, 08:30 im Besprechungsraum Elbe.', ''].join('\\n') },\n    { name: 'Urlaubsplanung_Q4.csv', mime: 'text/csv', content: [\n      'Name;Von;Bis;Vertretung', 'Jonas;17.11.;21.11.;Murat', 'Sabine;22.12.;02.01.;Lea', 'Murat;15.12.;19.12.;Jonas',\n      'Lea;-;-;(Probezeit)', ''].join('\\n') },\n  ];\n}\n\n// Büro-Lage auswerten: reads the six answers from the DEMO spaces only (URLs fixed in \"Demo-Bereich festlegen\"), keeps a strict\n// whitelist of fields (titles, times, file names; never owners, user ids or real names: Talk messages count only if a demo bot\n// wrote them), finds the first free slot for the Fokuszeit, plans the cleanup (keep the newest 5 items THIS workflow created:\n// cards with the footer marker, Briefing-*.md in /n8n-Demo/Briefings/, n8n-fokus-*.ics in the n8n-demo calendar) and the refresh\n// of the seed data (due dates of the demo cards stay relative to today, demo files are re-uploaded when older than 20 h).\n// Then it writes the prompt for the AI assistant.\nconst cfg = $('Demo-Bereich festlegen').first().json;\nconst pick = n => { try { return $(n).first().json; } catch (e) { return null; } };\nconst txt = r => r ? (typeof r.data === 'string' ? r.data : typeof r.body === 'string' ? r.body : (r.data && typeof r.data === 'object' ? JSON.stringify(r.data) : '')) : '';\nconst okr = r => r && Number(r.statusCode) >= 200 && Number(r.statusCode) < 300;\nconst inDemo = u => cfg.allowed.some(p => String(u).startsWith(p));\nconst NC = cfg.urls.files.match(/^https?:\\/\\/[^\\/]+/)[0];\nconst lang = cfg.lang;\nconst now = Date.parse(cfg.now);\n\n// ---- Berlin time helpers (same as in \"Demo-Bereich festlegen\")\nconst TZ = 'Europe/Berlin';\nconst parts = ts => { const o = {}; for (const p of new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit',\n  hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', weekday: 'short' }).formatToParts(new Date(ts))) o[p.type] = p.value; return o; };\nconst offset = ts => { const p = parts(ts); return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second) - Math.floor(ts / 1000) * 1000; };\nconst local = (y, m, d, h, mi) => { const g = Date.UTC(y, m - 1, d, h, mi); let t = g - offset(g); t = g - offset(t); return t; };\nconst ymd = t => { const q = parts(t); return `${q.year}-${q.month}-${q.day}`; };\nconst hm = t => { const q = parts(t); return `${q.hour}:${q.minute}`; };\nconst P = parts(now);\nconst at = (dd, h, mi) => local(+P.year, +P.month, +P.day + dd, h, mi);\nconst xmlDec = s => String(s).replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '\"').replace(/&#039;|&apos;/g, \"'\")\n  .replace(/&#13;|&#x0?D;/gi, '').replace(/&#(\\d+);/g, (_, n) => String.fromCharCode(+n)).replace(/&amp;/g, '&');\nconst clean = (s, n) => String(s == null ? '' : s).replace(/[\\u0000-\\u001f]+/g, ' ').replace(/\\s+/g, ' ').trim().slice(0, n);\n\n// ---- WebDAV multistatus → [{href, props}]\nconst responses = xml => {\n  const out = [];\n  for (const m of String(xml).matchAll(/<(?:\\w+:)?response\\b[\\s\\S]*?<\\/(?:\\w+:)?response>/gi)) {\n    const b = m[0];\n    const href = xmlDec(((b.match(/<(?:\\w+:)?href>([\\s\\S]*?)<\\/(?:\\w+:)?href>/i) || [])[1] || '').trim());\n    const prop = n => { const r = b.match(new RegExp('<(?:\\\\w+:)?' + n + '(?:\\\\s[^>]*)?>([\\\\s\\\\S]*?)<\\\\/(?:\\\\w+:)?' + n + '>', 'i')); return r ? xmlDec(r[1]) : ''; };\n    out.push({ href, b, prop, dir: /<(?:\\w+:)?collection\\s*\\/>/i.test(b) });\n  }\n  return out;\n};\n\n// ---- calendar: VEVENTs (expanded by the server; a small RRULE fallback for WEEKLY/DAILY if it is not)\nconst icsDate = (v, tzid) => {\n  const m = String(v).match(/^(\\d{4})(\\d{2})(\\d{2})(?:T(\\d{2})(\\d{2})(\\d{2})?(Z)?)?$/);\n  if (!m) return null;\n  if (!m[4]) return local(+m[1], +m[2], +m[3], 0, 0);\n  if (m[7]) return Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +(m[6] || 0));\n  return local(+m[1], +m[2], +m[3], +m[4], +m[5]);              // TZID=Europe/Berlin or floating: Berlin\n};\nconst vevents = ics => {\n  const lines = String(ics).replace(/\\r\\n[ \\t]/g, '').replace(/\\n[ \\t]/g, '').split(/\\r?\\n/);\n  const evs = []; let cur = null;\n  for (const l of lines) {\n    if (l === 'BEGIN:VEVENT') cur = {};\n    else if (l === 'END:VEVENT') { if (cur) evs.push(cur); cur = null; }\n    else if (cur) { const i = l.indexOf(':'); if (i > 0) { const k = l.slice(0, i).split(';')[0].toUpperCase(); if (!(k in cur)) cur[k] = l.slice(i + 1); } }\n  }\n  return evs;\n};\nconst unesc = s => String(s || '').replace(/\\\\n/gi, ' ').replace(/\\\\([,;\\\\])/g, '$1');\nconst R0 = Date.parse(cfg.range[0]), R1 = Date.parse(cfg.range[1]);\nconst events = [];\nconst calR = pick('Kalender: Termine heute & morgen');\nif (okr(calR)) {\n  for (const r of responses(txt(calR))) {\n    if (!inDemo(NC + r.href) && !inDemo(r.href)) continue;\n    for (const e of vevents(r.prop('calendar-data'))) {\n      const s = icsDate(e.DTSTART), en = icsDate(e.DTEND) || (s + 3600000);\n      if (s == null) continue;\n      const base = { uid: String(e.UID || ''), t: clean(unesc(e.SUMMARY), 80), where: clean(unesc(e.LOCATION), 40) };\n      const dur = en - s;\n      const rr = String(e.RRULE || '');\n      if (rr && !e['RECURRENCE-ID']) {                          // fallback expansion (server did not expand)\n        const by = (rr.match(/BYDAY=([A-Z,]+)/) || [])[1];\n        const freq = (rr.match(/FREQ=(\\w+)/) || [])[1];\n        for (let d = 0; d < 2; d++) {\n          const q = parts(at(d, 12, 0));\n          const dayCode = { Mon: 'MO', Tue: 'TU', Wed: 'WE', Thu: 'TH', Fri: 'FR', Sat: 'SA', Sun: 'SU' }[q.weekday];\n          if (freq === 'WEEKLY' && by && !by.split(',').includes(dayCode)) continue;\n          if (!['WEEKLY', 'DAILY'].includes(freq)) continue;\n          const sp = parts(s);\n          const st = at(d, +sp.hour, +sp.minute);\n          if (st >= s) events.push({ ...base, s: st, e: st + dur });\n        }\n      } else if (en > R0 && s < R1) events.push({ ...base, s, e: en });\n    }\n  }\n}\nevents.sort((a, b) => a.s - b.s);\nconst isFokus = e => /^n8n-fokus-/.test(e.uid);\nconst appts = events.filter(e => !isFokus(e)).map(e => ({ d: ymd(e.s) === cfg.today ? 'today' : 'tomorrow', s: hm(e.s), e: hm(e.e), t: e.t, where: e.where, _s: e.s, _e: e.e }))\n  .filter(e => e.t).slice(0, 14);\n\n// ---- first free slot (Fokuszeit): working hours 08:00-18:00 Berlin, at least 60 min, at most 90; today, else tomorrow.\n// Earlier Fokuszeit entries of this workflow are ignored, so the slot is stable and they never pile up in the timeline.\nconst q15 = t => Math.ceil(t / 900000) * 900000;\nconst slot = (() => {\n  for (let d = 0; d < 2; d++) {\n    const ws = at(d, 8, 0), we = at(d, 18, 0);\n    let t = d === 0 ? Math.max(ws, q15(now + 15 * 60000)) : ws;\n    const busy = appts.filter(a => a._e > ws && a._s < we).map(a => [a._s, a._e]).sort((a, b) => a[0] - b[0]);\n    for (const [bs, be] of busy.concat([[we, we]])) {\n      if (bs - t >= 60 * 60000) return { s: t, e: Math.min(bs, t + 90 * 60000), d };\n      t = Math.max(t, be);\n    }\n  }\n  return null;\n})();\nconst gaps = (() => {                                            // free blocks of today (for the AI: buffer suggestion)\n  const out = [], ws = at(0, 8, 0), we = at(0, 18, 0);\n  let t = Math.max(ws, q15(now));\n  for (const [bs, be] of appts.filter(a => a.d === 'today').map(a => [a._s, a._e]).concat([[we, we]])) {\n    if (bs - t >= 30 * 60000 && bs <= we) out.push(`${hm(t)}-${hm(bs)}`);\n    t = Math.max(t, be);\n  }\n  return out.slice(0, 5);\n})();\n\n// ---- calendar entries for cleanup: only n8n-fokus-*.ics inside the demo calendar\nconst calList = pick('Kalender: Einträge auflisten');\nconst fokusOld = okr(calList) ? responses(txt(calList)).map(r => r.href).filter(h => /\\/n8n-fokus-[\\w-]+\\.ics$/.test(h) && inDemo(NC + h)).sort() : [];\n\n// ---- Deck: board 1 only\nconst deckR = pick('Deck: Board lesen');\nlet stacks = [];\nif (okr(deckR)) { try { stacks = typeof deckR.body === 'object' && deckR.body ? deckR.body : JSON.parse(txt(deckR)); } catch (e) { stacks = []; } }\nif (!Array.isArray(stacks)) stacks = [];\nconst MARK = 'erstellt von n8n · Büro-Morgen';\nconst cards = [], mine = [], deckRefresh = [];\nfor (const st of stacks) {\n  if (Number(st.boardId) !== cfg.deck_board) continue;\n  for (const c of (st.cards || [])) {\n    const desc = String(c.description || '');\n    if (desc.includes(MARK)) { mine.push({ id: c.id, stack: st.id }); continue; }\n    if (c.archived) continue;\n    let due = c.duedate ? Date.parse(c.duedate) : null;\n    const off = (desc.match(/demo-due:(-?\\d+)/) || [])[1];\n    if (off != null) {                                            // seed card: due date stays relative to today, 16:00 Berlin\n      const want = at(+off, 16, 0);\n      if (due == null || Math.abs(due - want) > 3600000) {\n        deckRefresh.push({ method: 'PUT', url: `${cfg.urls.deck_stacks}/${st.id}/cards/${c.id}`, ctype: 'application/json',\n          body: JSON.stringify({ title: c.title, type: c.type || 'plain', owner: (c.owner && c.owner.uid) || c.owner, order: c.order,\n            description: desc, duedate: new Date(want).toISOString() }) });\n      }\n      due = want;\n    }\n    const done = st.title === 'Erledigt' || !!c.done;\n    if (done) continue;\n    const dd = due == null ? null : Math.round((Date.parse(ymd(due) + 'T12:00:00Z') - Date.parse(cfg.today + 'T12:00:00Z')) / 86400000);\n    cards.push({ t: clean(c.title, 80), stack: clean(st.title, 24), due: due == null ? null : ymd(due), days: dd,\n      st: dd == null ? 'open' : due < now ? 'overdue' : dd === 0 ? 'today' : dd <= 3 ? 'soon' : 'later',\n      labels: (c.labels || []).map(l => clean(l.title, 20)).filter(Boolean).slice(0, 3) });\n  }\n}\nconst rank = { overdue: 0, today: 1, soon: 2, later: 3, open: 4 };\ncards.sort((a, b) => rank[a.st] - rank[b.st] || (a.days ?? 99) - (b.days ?? 99));\nmine.sort((a, b) => b.id - a.id);\nconst deckOld = mine.slice(4);                                   // + the new one = 5\n\n// ---- files: /n8n-Demo/ (seed files) and /n8n-Demo/Briefings/\nconst seed = demoFiles(new Date(now));\nconst fileR = pick('Dateien: Demo-Ordner lesen');\nconst seen = {};\nconst files = [];\nif (okr(fileR)) {\n  for (const r of responses(txt(fileR))) {\n    if (r.dir || !inDemo(NC + r.href)) continue;\n    let name = r.href.split('/').pop();\n    try { name = decodeURIComponent(name); } catch (e) {}\n    const mt = Date.parse(r.prop('getlastmodified')) || 0;\n    seen[name] = mt;\n    files.push({ n: clean(name, 80), kb: Math.max(1, Math.round(Number(r.prop('getcontentlength') || 0) / 1024)), _m: mt });\n  }\n}\nconst fileRefresh = [];\nfor (const f of seed) {                                          // missing or older than 20 h → upload again (fresh mtime)\n  if (!seen[f.name] || now - seen[f.name] > 20 * 3600000) {\n    fileRefresh.push({ method: 'PUT', url: cfg.urls.files + encodeURIComponent(f.name), ctype: f.mime, body: f.content });\n    const x = files.find(y => y.n === f.name);\n    if (x) x._m = now; else files.push({ n: f.name, kb: Math.max(1, Math.round(f.content.length / 1024)), _m: now });\n  }\n}\nconst kind = n => (n.match(/\\.(pdf|md|csv|docx?|xlsx?|odt|ods|txt|png|jpe?g)$/i) || [, 'file'])[1].toLowerCase();\nconst fileList = files.map(f => ({ n: f.n, kind: kind(f.n), kb: f.kb, ago: Math.max(0, Math.round((now - f._m) / 60000)), fresh: now - f._m < 24 * 3600000 }))\n  .sort((a, b) => a.ago - b.ago).slice(0, 8);\n\nconst brR = pick('Dateien: Briefings lesen');\nconst briefOld = okr(brR) ? responses(txt(brR)).map(r => r.href).filter(h => /\\/Briefing-[\\w-]+\\.md$/.test(h) && inDemo(NC + h)).sort() : [];\n\n// ---- Talk: only messages written by the demo bots in the demo room (never messages of real users)\nconst talkR = pick('Talk: Team-Chat lesen');\nlet msgs = [];\nif (okr(talkR)) {\n  let j = talkR.body; if (typeof j !== 'object' || !j) { try { j = JSON.parse(txt(talkR)); } catch (e) { j = {}; } }\n  msgs = ((j.ocs && j.ocs.data) || []).filter(m => m && m.actorType === 'bots' && m.messageType === 'comment' && !m.systemMessage\n    && /\\(.*Demo\\)$/.test(String(m.actorDisplayName || '')))\n    .sort((a, b) => a.id - b.id).slice(-5)\n    .map(m => ({ who: clean(String(m.actorDisplayName).replace(/,\\s*Demo\\)$/, ')'), 40), text: clean(m.message, 220) }));\n}\n\n// ---- cleanup and refresh list (executed after the three writes)\nconst cleanup = []\n  .concat(deckOld.map(c => ({ method: 'DELETE', url: `${cfg.urls.deck_stacks}/${c.stack}/cards/${c.id}`, kind: 'card' })))\n  .concat(briefOld.slice(0, Math.max(0, briefOld.length - 4)).map(h => ({ method: 'DELETE', url: NC + h, kind: 'file' })))\n  .concat(fokusOld.slice(0, Math.max(0, fokusOld.length - 4)).map(h => ({ method: 'DELETE', url: NC + h, kind: 'event' })));\nconst refresh = deckRefresh.concat(fileRefresh);\nfor (const x of cleanup.concat(refresh)) if (!inDemo(x.url)) throw new Error('outside the demo space: ' + x.url);\n\n// ---- prompt\nconst LANG = { de: 'German, informal \"du\"', en: 'English', it: 'Italian, informal \"tu\"' }[lang];\nconst dayName = new Intl.DateTimeFormat({ de: 'de-DE', en: 'en-GB', it: 'it-IT' }[lang], { timeZone: TZ, weekday: 'long', day: 'numeric', month: 'long' }).format(new Date(now));\nconst facts = {\n  now: `${dayName}, ${hm(now)}`,\n  appointments_today: appts.filter(a => a.d === 'today').map(a => `${a.s}-${a.e} ${a.t}`),\n  appointments_tomorrow: appts.filter(a => a.d === 'tomorrow').map(a => `${a.s}-${a.e} ${a.t}`),\n  free_blocks_today: gaps,\n  focus_block_booked: slot ? `${slot.d ? 'tomorrow' : 'today'} ${hm(slot.s)}-${hm(slot.e)}` : 'none',\n  deck_cards: cards.slice(0, 9).map(c => ({ title: c.t, column: c.stack, due: c.st === 'overdue' ? `overdue (${-c.days} d)` : c.st === 'today' ? 'today' : c.due || 'no date', labels: c.labels })),\n  new_files_24h: fileList.filter(f => f.fresh).map(f => f.n),\n  unread_team_chat: msgs.map(m => `${m.who}: ${m.text}`),\n  team: ['Jonas (Vertrieb)', 'Sabine (Buchhaltung)', 'Murat (Projekte)', 'Lea (Assistenz, new)'],\n};\nconst prompt = 'You are the morning assistant of the small office \"Musterbüro GmbH\" (a fictional demo company). '\n  + `Write in ${LANG}. Here is this morning's situation from Nextcloud (use ONLY these facts, never invent appointments, people, numbers or files): `\n  + JSON.stringify(facts) + '. '\n  + 'Answer with JSON only: {\"greeting\":\"\",\"top3\":[{\"title\":\"\",\"why\":\"\",\"minutes\":30}],\"buffer\":\"\",\"delegate\":[{\"task\":\"\",\"to\":\"\",\"why\":\"\"}],\"chat\":\"\",\"tip\":\"\",\"signoff\":\"\"}. '\n  + 'greeting: one friendly, witty sentence (max 140 chars) about the day ahead. '\n  + 'top3: exactly 3 priorities for today, most urgent first (overdue and due-today cards, things the chat asks for); title max 60 chars, imperative; '\n  + 'why max 110 chars, refers to a concrete fact; minutes 15-120. '\n  + 'buffer: one sentence (max 140 chars) that names one free block from free_blocks_today as a buffer for surprises (not the focus block). '\n  + 'delegate: 1-2 tasks to hand over to a team member from \"team\", with a short why (max 90 chars). '\n  + 'chat: one sentence (max 140 chars) on what to answer in the team chat first. '\n  + 'tip: one short productivity tip that fits the day (max 120 chars). signoff: max 60 chars, efficient and a bit funny. '\n  + 'No markdown, no emojis, no hashtags.';\nreturn [{ json: {\n  state: { id: cfg.id, lang, now: cfg.now, today: cfg.today, day: dayName, hm: hm(now),\n    events: appts.map(({ _s, _e, ...a }) => a), cards: cards.slice(0, 10), files: fileList, chat: msgs, gaps,\n    slot: slot ? { s: new Date(slot.s).toISOString(), e: new Date(slot.e).toISOString(), hs: hm(slot.s), he: hm(slot.e), d: slot.d ? 'tomorrow' : 'today', day: ymd(slot.s) } : null,\n    read_ok: { cal: okr(calR), deck: okr(deckR), files: okr(fileR), talk: okr(talkR) } },\n  cleanup, refresh,\n  body: { model: 'openai/gpt-oss-120b', temperature: 0.7, max_tokens: 3000, reasoning_effort: 'low', response_format: { type: 'json_object' },\n          messages: [{ role: 'user', content: prompt }] } } }];"
   },
   "id": "c1",
   "name": "Büro-Lage auswerten",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    980,
    400
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://openai.inference.de-txl.ionos.com/v1/chat/completions",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify($json.body) }}",
    "options": {
     "timeout": 60000
    }
   },
   "id": "a1",
   "name": "Assistent schreibt (KI)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1200,
    400
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpHeaderAuth": {
     "id": "",
     "name": "YOUR OpenAI-compatible API key (Header Auth: Authorization = Bearer …)"
    }
   }
  },
  {
   "parameters": {
    "mode": "runOnceForAllItems",
    "jsCode": "// Briefing prüfen: reads the AI answer (falls back to a template from the same facts), then prepares the three writes into the\n// demo spaces: the Deck card «Heute: Top 3» with a checklist, Briefing-<date>.md for /n8n-Demo/Briefings/ and the Fokuszeit event\n// (n8n-fokus-*.ics) for the n8n-demo calendar.\nconst A = $('Büro-Lage auswerten').first().json;\nconst S = A.state;\nconst cfg = $('Demo-Bereich festlegen').first().json;\nconst lang = S.lang;\nconst clean = (s, n) => String(s == null ? '' : s).replace(/[*_#`>\\[\\]]/g, '').replace(/\\s+/g, ' ').trim().slice(0, n);\nlet j = {};\ntry { const m = String($input.first().json.choices[0].message.content).match(/\\{[\\s\\S]*\\}/); j = JSON.parse(m[0]); } catch (e) { j = {}; }\nconst L = {\n  de: { hi: 'Guten Morgen! Kaffee in die Hand, der Tag ist sortiert.', why: 'Steht im Deck', del: 'Lea', delw: 'Gute Übung für die neue Kollegin.',\n        buf: 'Halte {b} frei, falls etwas dazwischenkommt.', nobuf: 'Plane zwischen den Terminen jeweils zehn Minuten Luft ein.',\n        chat: 'Antworte zuerst auf die Frage zum Angebot, das hat die knappste Frist.', tip: 'Erst das Unangenehmste, dann die Mails.',\n        bye: 'Du schaffst das. Und wenn nicht: Kaffee hilft.', top: 'Top 3 für heute', title: 'Briefing', appt: 'Termine heute', fokus: 'Fokuszeit',\n        dele: 'Delegieren', bufh: 'Puffer', chath: 'Team-Chat', tiph: 'Tipp', none: 'keine', made: 'Erstellt von n8n · Büro-Morgen (Demo) · fiktive Daten der Musterbüro GmbH' },\n  en: { hi: 'Good morning! Grab a coffee, the day is sorted.', why: 'On the Deck board', del: 'Lea', delw: 'Good practice for the new colleague.',\n        buf: 'Keep {b} free in case something comes up.', nobuf: 'Leave ten minutes of air between meetings.',\n        chat: 'Answer the question about the quote first, it has the tightest deadline.', tip: 'Do the unpleasant thing first, then the e-mails.',\n        bye: 'You have got this. And if not: coffee helps.', top: 'Top 3 for today', title: 'Briefing', appt: 'Appointments today', fokus: 'Focus time',\n        dele: 'Delegate', bufh: 'Buffer', chath: 'Team chat', tiph: 'Tip', none: 'none', made: 'Created by n8n · Office morning (demo) · fictional data of Musterbüro GmbH' },\n  it: { hi: 'Buongiorno! Caffè in mano, la giornata è in ordine.', why: 'È sul Deck', del: 'Lea', delw: 'Buon allenamento per la nuova collega.',\n        buf: 'Tieni libero {b} per gli imprevisti.', nobuf: 'Lascia dieci minuti d\\'aria tra un appuntamento e l\\'altro.',\n        chat: 'Rispondi prima alla domanda sull\\'offerta, ha la scadenza più stretta.', tip: 'Prima la cosa più sgradevole, poi le e-mail.',\n        bye: 'Ce la fai. E se no: il caffè aiuta.', top: 'Top 3 di oggi', title: 'Briefing', appt: 'Appuntamenti di oggi', fokus: 'Tempo di focus',\n        dele: 'Da delegare', bufh: 'Buffer', chath: 'Chat del team', tiph: 'Consiglio', none: 'nessuno', made: 'Creato da n8n · Mattina in ufficio (demo) · dati fittizi della Musterbüro GmbH' },\n}[lang];\n\nlet top3 = (Array.isArray(j.top3) ? j.top3 : []).map(x => ({ t: clean(x && x.title, 70), why: clean(x && x.why, 130),\n  min: Math.max(15, Math.min(120, Math.round(Number(x && x.minutes) || 30))) })).filter(x => x.t.length >= 4).slice(0, 3);\nconst ai = top3.length === 3;\nconst urgent = c => (c.labels.includes('Dringend') ? 0 : 1) + ({ overdue: 0, today: 2, soon: 4, later: 6, open: 8 }[c.st] ?? 8);\nif (!ai) top3 = S.cards.slice().sort((a, b) => urgent(a) - urgent(b)).slice(0, 3)\n  .map(c => ({ t: c.t, why: `${L.why}: ${c.stack}${c.due ? ' · ' + c.due : ''}`, min: 45 }));\nwhile (top3.length < 3) top3.push({ t: L.tip, why: '', min: 15 });\nconst TEAM = ['Jonas', 'Sabine', 'Murat', 'Lea'];\nlet delegate = (Array.isArray(j.delegate) ? j.delegate : []).map(x => ({ task: clean(x && x.task, 80), to: clean(x && x.to, 30), why: clean(x && x.why, 100) }))\n  .filter(x => x.task && TEAM.some(n => x.to.startsWith(n))).slice(0, 2);\nif (!delegate.length) {\n  const free = S.cards.filter(c => !top3.some(t => t.t === c.t) && !c.t.includes(L.del));\n  const c = free.find(c => c.labels.includes('Intern')) || free[free.length - 1];\n  if (c) delegate = [{ task: c.t, to: L.del, why: L.delw }];\n}\nconst greeting = clean(j.greeting, 160) || L.hi;\nconst buffer = clean(j.buffer, 160) || (S.gaps.length ? L.buf.replace('{b}', S.gaps[S.gaps.length - 1]) : L.nobuf);\nconst chat = clean(j.chat, 160) || (S.chat.length ? L.chat : '');\nconst tip = clean(j.tip, 140) || L.tip;\nconst signoff = clean(j.signoff, 70) || L.bye;\n\n// ---- the three writes\nconst stamp = S.now.replace(/\\.\\d+Z$|Z$/, '').replace(/[-:]/g, '').replace('T', '');                 // 20261108073301 (UTC)\nconst P = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' })\n  .formatToParts(new Date(S.now)).reduce((o, p) => (o[p.type] = p.value, o), {});\nconst fname = `Briefing-${P.year}-${P.month}-${P.day}_${P.hour}-${P.minute}-${P.second}.md`;\nconst dShort = `${P.day}.${P.month}.`;\nconst appts = S.events.filter(e => e.d === 'today');\nconst md = [`# ${L.title} · ${S.day}`, '', `> ${greeting}`, '', `## ${L.top}`, ...top3.map((t, i) => `${i + 1}. **${t.t}** (${t.min} min)${t.why ? ' – ' + t.why : ''}`), '',\n  `## ${L.appt}`, ...(appts.length ? appts.map(e => `- ${e.s}–${e.e} ${e.t}`) : [`- ${L.none}`]),\n  ...(S.slot ? [`- **${S.slot.hs}–${S.slot.he} ${L.fokus}** (${S.slot.d === 'today' ? '' : S.slot.day + ', '}n8n)`] : []), '',\n  `## ${L.bufh}`, buffer, '', `## ${L.dele}`, ...delegate.map(d => `- ${d.task} → **${d.to}**${d.why ? ' (' + d.why + ')' : ''}`), '',\n  ...(chat ? [`## ${L.chath}`, chat, ''] : []), `## ${L.tiph}`, tip, '', `_${signoff}_`, '', '---', `_${L.made}_`, ''].join('\\n');\nconst card = { title: `Heute: Top 3 · ${dShort}`, type: 'plain', order: -1,\n  description: top3.map(t => `- [ ] ${t.t} (${t.min} min)`).join('\\n') + '\\n\\n' + (delegate.length ? `${L.dele}: ` + delegate.map(d => `${d.task} → ${d.to}`).join('; ') + '\\n\\n' : '')\n    + '---\\n_erstellt von n8n · Büro-Morgen (Demo)_',\n  duedate: new Date(Date.UTC(+P.year, +P.month - 1, +P.day, 16, 0)).toISOString() };\nconst fold = s => s.replace(/[\\\\;,]/g, m => '\\\\' + m);\nconst ics = S.slot ? ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//daniele-michelin.com//n8n Buero-Morgen//DE', 'BEGIN:VEVENT',\n  `UID:n8n-fokus-${stamp}-${S.id}@daniele-michelin.com`, `DTSTAMP:${stamp.slice(0, 8)}T${stamp.slice(8)}Z`,\n  `DTSTART:${S.slot.s.replace(/[-:]/g, '').replace(/\\.\\d+Z$/, 'Z')}`, `DTEND:${S.slot.e.replace(/[-:]/g, '').replace(/\\.\\d+Z$/, 'Z')}`,\n  `SUMMARY:${fold('Fokuszeit: ' + top3[0].t).slice(0, 120)}`, `DESCRIPTION:${fold('Geblockt von n8n · Büro-Morgen (Demo). Fiktive Daten.')}`,\n  'TRANSP:OPAQUE', 'CATEGORIES:n8n-Demo', 'END:VEVENT', 'END:VCALENDAR', ''].join('\\r\\n') : '';\nreturn [{ json: { ...S, ai, brief: { greeting, top3, buffer, delegate, chat, tip, signoff },\n  write: { card, card_url: cfg.urls.deck_today, file: fname, file_url: cfg.urls.briefings + encodeURIComponent(fname), md,\n           ics, ics_url: S.slot ? cfg.urls.cal_list + `n8n-fokus-${stamp}-${S.id}.ics` : '' } } }];"
   },
   "id": "c2",
   "name": "Briefing prüfen",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1420,
    400
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "={{ $('Briefing prüfen').first().json.write.card_url }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "OCS-APIRequest",
       "value": "true"
      },
      {
       "name": "Accept",
       "value": "application/json"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify($('Briefing prüfen').first().json.write.card) }}"
   },
   "id": "x1",
   "name": "Deck: Karte «Heute: Top 3» anlegen",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1640,
    400
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "method": "PUT",
    "url": "={{ $('Briefing prüfen').first().json.write.file_url }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "OCS-APIRequest",
       "value": "true"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    },
    "sendBody": true,
    "contentType": "raw",
    "rawContentType": "text/markdown",
    "body": "={{ $('Briefing prüfen').first().json.write.md }}"
   },
   "id": "x2",
   "name": "Dateien: Briefing speichern",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1860,
    400
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "method": "PUT",
    "url": "={{ $('Briefing prüfen').first().json.write.ics_url }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "If-None-Match",
       "value": "*"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    },
    "sendBody": true,
    "contentType": "raw",
    "rawContentType": "text/calendar",
    "body": "={{ $('Briefing prüfen').first().json.write.ics }}"
   },
   "id": "x3",
   "name": "Kalender: Fokuszeit blocken",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2080,
    400
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "mode": "runOnceForAllItems",
    "jsCode": "// Ergebnis zusammenstellen: the receipts of the three writes (status codes of the Nextcloud answers) plus the whole morning for the\n// page. Only demo data and the generated texts go out; no URLs, user names or ids of the Nextcloud account.\nconst B = $('Briefing prüfen').first().json;\nconst A = $('Büro-Lage auswerten').first().json;\nconst pick = n => { try { return $(n).first().json; } catch (e) { return null; } };\nconst code = r => r ? Number(r.statusCode || 0) : 0;\nconst okc = r => code(r) >= 200 && code(r) < 300;\nconst rc = pick('Deck: Karte «Heute: Top 3» anlegen'), rf = pick('Dateien: Briefing speichern'), re = pick('Kalender: Fokuszeit blocken');\nconst { write, brief, ...state } = B;\nconst n = k => A.cleanup.filter(x => x.kind === k).length;\nreturn [{ json: {\n  id: state.id, lang: state.lang, day: state.day, hm: state.hm, today: state.today,\n  events: state.events, cards: state.cards, files: state.files, chat: state.chat, ai: state.ai, read_ok: state.read_ok,\n  brief: { ...brief, md: write.md },\n  receipts: {\n    deck: { ok: okc(rc), title: write.card.title, items: brief.top3.map(t => t.t), stack: 'Heute', board: 'Büro-Morgen (Demo)' },\n    file: { ok: okc(rf), name: write.file, folder: 'n8n-Demo/Briefings', kb: Math.max(1, Math.round(write.md.length / 1024)) },\n    event: { ok: okc(re) && !!state.slot, title: 'Fokuszeit', d: state.slot ? state.slot.d : null, day: state.slot ? state.slot.day : null,\n             s: state.slot ? state.slot.hs : null, e: state.slot ? state.slot.he : null },\n  },\n  cleaned: { cards: n('card'), files: n('file'), events: n('event') },\n} }];"
   },
   "id": "c3",
   "name": "Ergebnis zusammenstellen",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2300,
    300
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://YOUR-SITE.example/office-ingest.php",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "X-Office-Token",
       "value": "YOUR-SECRET-TOKEN"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify($json) }}",
    "options": {
     "timeout": 60000
    }
   },
   "id": "p1",
   "name": "Ergebnis an die Seite",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2520,
    300
   ]
  },
  {
   "parameters": {
    "mode": "runOnceForAllItems",
    "jsCode": "// Aufräumen & Auffrischen: one item per request. DELETE = items this workflow created earlier (only the ones beyond the newest 5,\n// only inside the demo spaces); PUT = seed data of the demo (relative due dates of the demo cards, demo files older than 20 h).\n// The URLs were checked against the demo space in \"Büro-Lage auswerten\"; checked again here.\nconst A = $('Büro-Lage auswerten').first().json;\nconst allowed = $('Demo-Bereich festlegen').first().json.allowed;\nconst items = A.cleanup.map(x => ({ method: 'DELETE', url: x.url }));   // only the DELETE part\nreturn items.filter(x => allowed.some(p => x.url.startsWith(p))).map(x => ({ json: x }));"
   },
   "id": "c4",
   "name": "Aufräumen: Liste (älter als die letzten 5)",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2300,
    520
   ]
  },
  {
   "parameters": {
    "method": "DELETE",
    "url": "={{ $json.url }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "OCS-APIRequest",
       "value": "true"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    }
   },
   "id": "d1",
   "name": "Aufräumen (DELETE)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2520,
    520
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  },
  {
   "parameters": {
    "mode": "runOnceForAllItems",
    "jsCode": "// Aufräumen & Auffrischen: one item per request. DELETE = items this workflow created earlier (only the ones beyond the newest 5,\n// only inside the demo spaces); PUT = seed data of the demo (relative due dates of the demo cards, demo files older than 20 h).\n// The URLs were checked against the demo space in \"Büro-Lage auswerten\"; checked again here.\nconst A = $('Büro-Lage auswerten').first().json;\nconst allowed = $('Demo-Bereich festlegen').first().json.allowed;\nconst items = A.refresh;   // only the PUT part (seed data of the demo)\nreturn items.filter(x => allowed.some(p => x.url.startsWith(p))).map(x => ({ json: x }));"
   },
   "id": "c5",
   "name": "Auffrischen: Liste (Demo-Daten)",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2300,
    700
   ]
  },
  {
   "parameters": {
    "method": "PUT",
    "url": "={{ $json.url }}",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBasicAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "OCS-APIRequest",
       "value": "true"
      }
     ]
    },
    "options": {
     "webdavMethods": true,
     "timeout": 20000,
     "response": {
      "response": {
       "fullResponse": true,
       "neverError": true,
       "responseFormat": "text"
      }
     }
    },
    "sendBody": true,
    "contentType": "raw",
    "rawContentType": "={{ $json.ctype }}",
    "body": "={{ $json.body }}"
   },
   "id": "u1",
   "name": "Auffrischen (PUT)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2520,
    700
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpBasicAuth": {
     "id": "",
     "name": "YOUR Nextcloud app password (Basic Auth: user + app password)"
    }
   }
  }
 ],
 "connections": {
  "Besucher-Knopf (Webhook)": {
   "main": [
    [
     {
      "node": "Demo-Bereich festlegen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Demo-Bereich festlegen": {
   "main": [
    [
     {
      "node": "Kalender: Termine heute & morgen",
      "type": "main",
      "index": 0
     },
     {
      "node": "Kalender: Einträge auflisten",
      "type": "main",
      "index": 0
     },
     {
      "node": "Deck: Board lesen",
      "type": "main",
      "index": 0
     },
     {
      "node": "Dateien: Demo-Ordner lesen",
      "type": "main",
      "index": 0
     },
     {
      "node": "Dateien: Briefings lesen",
      "type": "main",
      "index": 0
     },
     {
      "node": "Talk: Team-Chat lesen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Kalender: Termine heute & morgen": {
   "main": [
    [
     {
      "node": "Alles einsammeln",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Kalender: Einträge auflisten": {
   "main": [
    [
     {
      "node": "Alles einsammeln",
      "type": "main",
      "index": 1
     }
    ]
   ]
  },
  "Deck: Board lesen": {
   "main": [
    [
     {
      "node": "Alles einsammeln",
      "type": "main",
      "index": 2
     }
    ]
   ]
  },
  "Dateien: Demo-Ordner lesen": {
   "main": [
    [
     {
      "node": "Alles einsammeln",
      "type": "main",
      "index": 3
     }
    ]
   ]
  },
  "Dateien: Briefings lesen": {
   "main": [
    [
     {
      "node": "Alles einsammeln",
      "type": "main",
      "index": 4
     }
    ]
   ]
  },
  "Talk: Team-Chat lesen": {
   "main": [
    [
     {
      "node": "Alles einsammeln",
      "type": "main",
      "index": 5
     }
    ]
   ]
  },
  "Alles einsammeln": {
   "main": [
    [
     {
      "node": "Büro-Lage auswerten",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Büro-Lage auswerten": {
   "main": [
    [
     {
      "node": "Assistent schreibt (KI)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Assistent schreibt (KI)": {
   "main": [
    [
     {
      "node": "Briefing prüfen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Briefing prüfen": {
   "main": [
    [
     {
      "node": "Deck: Karte «Heute: Top 3» anlegen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Deck: Karte «Heute: Top 3» anlegen": {
   "main": [
    [
     {
      "node": "Dateien: Briefing speichern",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Dateien: Briefing speichern": {
   "main": [
    [
     {
      "node": "Kalender: Fokuszeit blocken",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Kalender: Fokuszeit blocken": {
   "main": [
    [
     {
      "node": "Ergebnis zusammenstellen",
      "type": "main",
      "index": 0
     },
     {
      "node": "Aufräumen: Liste (älter als die letzten 5)",
      "type": "main",
      "index": 0
     },
     {
      "node": "Auffrischen: Liste (Demo-Daten)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Ergebnis zusammenstellen": {
   "main": [
    [
     {
      "node": "Ergebnis an die Seite",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Aufräumen: Liste (älter als die letzten 5)": {
   "main": [
    [
     {
      "node": "Aufräumen (DELETE)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Auffrischen: Liste (Demo-Daten)": {
   "main": [
    [
     {
      "node": "Auffrischen (PUT)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "active": false,
 "settings": {
  "executionOrder": "v1",
  "timezone": "Europe/Berlin",
  "saveDataSuccessExecution": "none",
  "saveDataErrorExecution": "all"
 },
 "pinData": {}
}