{
 "name": "Data oracle (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,
    220
   ],
   "webhookId": "0f6c3c1e-5a43-5d1b-9a3e-0aac1e0a7a01"
  },
  {
   "parameters": {
    "url": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "User-Agent",
       "value": "daniele-michelin.com data oracle (+https://daniele-michelin.com/orakel/)"
      }
     ]
    },
    "options": {
     "timeout": 15000
    }
   },
   "id": "h1",
   "name": "Erde: Erdbeben (USGS)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    260,
    0
   ],
   "onError": "continueRegularOutput"
  },
  {
   "parameters": {
    "url": "https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "User-Agent",
       "value": "daniele-michelin.com data oracle (+https://daniele-michelin.com/orakel/)"
      }
     ]
    },
    "options": {
     "timeout": 15000
    }
   },
   "id": "h2",
   "name": "Himmel: Sonnensturm (NOAA)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    260,
    220
   ],
   "onError": "continueRegularOutput"
  },
  {
   "parameters": {
    "url": "https://api.wheretheiss.at/v1/satellites/25544",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "User-Agent",
       "value": "daniele-michelin.com data oracle (+https://daniele-michelin.com/orakel/)"
      }
     ]
    },
    "options": {
     "timeout": 15000
    }
   },
   "id": "h3",
   "name": "Wanderer: Wo ist die ISS?",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    260,
    440
   ],
   "onError": "continueRegularOutput"
  },
  {
   "parameters": {
    "url": "={{ 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&zoom=5&lat=' + $json.latitude + '&lon=' + $json.longitude + '&accept-language=' + (['de','en','it'].includes($('Besucher-Knopf (Webhook)').first().json.body.lang) ? $('Besucher-Knopf (Webhook)').first().json.body.lang : 'en') }}",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "User-Agent",
       "value": "daniele-michelin.com data oracle (+https://daniele-michelin.com/orakel/)"
      }
     ]
    },
    "options": {
     "timeout": 15000
    }
   },
   "id": "h4",
   "name": "Wanderer: Was liegt darunter?",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    480,
    440
   ],
   "onError": "continueRegularOutput"
  },
  {
   "parameters": {
    "numberInputs": 3
   },
   "id": "m1",
   "name": "Alle Omen sammeln",
   "type": "n8n-nodes-base.merge",
   "typeVersion": 3,
   "position": [
    720,
    220
   ]
  },
  {
   "parameters": {
    "mode": "runOnceForAllItems",
    "jsCode": "// Reads the three live omens, adds the moon (pure maths) and writes the prompt for the oracle.\nconst req = $('Besucher-Knopf (Webhook)').first().json.body || {};\nconst lang = ['de', 'en', 'it'].includes(req.lang) ? req.lang : 'en';\nconst topic = ['love', 'job', 'code', 'money', 'weekend', 'monday'].includes(req.topic) ? req.topic : 'job';\n\n// EARTH: strongest earthquake of the last 24 hours (USGS, magnitude 2.5+)\nlet quake = null;\ntry {\n  const fs = ($('Erde: Erdbeben (USGS)').first().json.features || []).filter(f => Date.now() - f.properties.time < 864e5);\n  const q = fs.reduce((a, b) => (!a || b.properties.mag > a.properties.mag) ? b : a, null);\n  if (q) quake = { mag: Math.round(q.properties.mag * 10) / 10, place: String(q.properties.place || '').replace(/^\\d+\\s*km\\s+[NSEW]{1,3}\\s+of\\s+/i, ''),\n                   ago_h: Math.max(0, Math.round((Date.now() - q.properties.time) / 36e5)), count: fs.length };\n} catch (e) {}\n\n// SKY: planetary K-index, the latest 3-hour value (NOAA SWPC); G-scale for geomagnetic storms\nlet sky = null;\ntry {\n  const rows = $('Himmel: Sonnensturm (NOAA)').all().map(i => i.json).filter(r => r && r.Kp != null);\n  const last = rows[rows.length - 1];\n  const kp = Math.round(Number(last.Kp) * 10) / 10;\n  sky = { kp, storm: kp >= 9 ? 'G5' : kp >= 8 ? 'G4' : kp >= 7 ? 'G3' : kp >= 6 ? 'G2' : kp >= 5 ? 'G1' : 'G0',\n          max24: Math.round(Math.max(...rows.slice(-8).map(r => Number(r.Kp))) * 10) / 10 };\n} catch (e) {}\n\n// WANDERER: the ISS right now, and what lies below\nconst iss = $('Wanderer: Wo ist die ISS?').first().json;\nlet geo = null; try { geo = $('Wanderer: Was liegt darunter?').first().json; } catch (e) {}\nconst lat = iss.latitude, lon = iss.longitude;\nconst OCEAN = { de: ['Nordpolarmeer', 'Südlicher Ozean', 'Atlantik', 'Indischer Ozean', 'Pazifik'],\n                en: ['Arctic Ocean', 'Southern Ocean', 'Atlantic Ocean', 'Indian Ocean', 'Pacific Ocean'],\n                it: ['Mar Glaciale Artico', 'Oceano Australe', 'Oceano Atlantico', 'Oceano Indiano', 'Oceano Pacifico'] }[lang];\nconst ocean = lat > 66 ? OCEAN[0] : lat < -55 ? OCEAN[1]\n  : (lat >= 0 ? (lon > -82 && lon < -5) : (lon > -68 && lon < 20)) ? OCEAN[2]\n  : (lon >= 20 && lon < 120 && lat < 25) ? OCEAN[3] : OCEAN[4];\nconst a = geo && !geo.error && geo.address ? geo.address : null;\nconst place = a ? [a.state || a.region || a.county || '', a.country || ''].filter(Boolean).join(', ') : ocean;\nconst wanderer = { place, lat: Math.round(lat * 100) / 100, lon: Math.round(lon * 100) / 100, kmh: Math.round(iss.velocity), day: iss.visibility === 'daylight' };\n\n// MOON: phase from a known new moon (6 Jan 2000, 18:14 UTC) and the synodic month\nconst syn = 29.530588853, age = (((Date.now() - Date.UTC(2000, 0, 6, 18, 14)) / 864e5) % syn + syn) % syn;\nconst ph = age / syn, illum = Math.round((1 - Math.cos(2 * Math.PI * ph)) / 2 * 100);\nconst k = Math.round(ph * 8) % 8;\nconst MOON = { de: ['Neumond', 'zunehmende Sichel', 'erstes Viertel', 'zunehmender Mond', 'Vollmond', 'abnehmender Mond', 'letztes Viertel', 'abnehmende Sichel'],\n               en: ['new moon', 'waxing crescent', 'first quarter', 'waxing gibbous', 'full moon', 'waning gibbous', 'last quarter', 'waning crescent'],\n               it: ['luna nuova', 'falce crescente', 'primo quarto', 'gibbosa crescente', 'luna piena', 'gibbosa calante', 'ultimo quarto', 'falce calante'] }[lang];\nconst moon = { name: MOON[k], illum, icon: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'][k] };\n\n// lucky number, honestly computed from the omens\nconst lucky = ((quake ? Math.round(quake.mag * 10) : 7) + Math.round((sky ? sky.kp : 2) * 3) + illum + Math.abs(Math.round(lat))) % 99 + 1;\n\nconst TOPIC = { love: 'love and relationships', job: 'work and career', code: 'programming and software projects', money: 'money (playful only, never financial advice)',\n                weekend: 'the coming weekend', monday: 'the next Monday' }[topic];\nconst LANG = { de: 'German, informal \"du\"', en: 'English', it: 'Italian, informal \"tu\"' }[lang];\nconst omens = { earth: quake, sky, wanderer, moon: { phase: moon.name, illumination_percent: illum } };\nconst prompt = 'You are the Data Oracle: a theatrical, warm, slightly ironic fortune teller who reads the future from live measurements of the planet. '\n  + `The visitor asks about: ${TOPIC}. Write in ${LANG}. Live omens: ${JSON.stringify(omens)}. `\n  + 'Answer with JSON only: {\"earth\":\"\",\"sky\":\"\",\"wanderer\":\"\",\"prophecy\":\"\",\"advice\":\"\"}. '\n  + 'earth, sky, wanderer: one short sentence each (max 85 characters) reading that omen for the topic, quoting its number exactly. '\n  + 'prophecy: 2-3 sentences (max 300 characters) weaving all omens and the moon together, mysterious, funny and encouraging, specific to the topic. '\n  + 'advice: one concrete, playful tip (max 70 characters). '\n  + 'Rules: an earthquake may have hurt people: only say that the earth stirred far away, never joke about damage or victims; '\n  + 'no real predictions about health, death or investments; no hashtags, no emojis.';\nreturn [{ json: { id: req.id, lang, topic, quake, sky, wanderer, moon, lucky,\n  body: { model: 'openai/gpt-oss-120b', temperature: 0.9, max_tokens: 4000, reasoning_effort: 'low', response_format: { type: 'json_object' },\n          messages: [{ role: 'user', content: prompt }] } } }];"
   },
   "id": "c1",
   "name": "Omen deuten",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    940,
    220
   ]
  },
  {
   "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": "Das Orakel spricht (KI)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1160,
    220
   ],
   "onError": "continueRegularOutput",
   "credentials": {
    "httpHeaderAuth": {
     "id": "",
     "name": "YOUR OpenAI-compatible API key (Header Auth: Authorization = Bearer …)"
    }
   }
  },
  {
   "parameters": {
    "mode": "runOnceForAllItems",
    "jsCode": "// Reads the AI answer; if it fails, the oracle falls back to a template so the visitor always gets a reading.\nconst d = $('Omen deuten').first().json;\nlet c = null;\ntry { const m = String($input.first().json.choices[0].message.content).match(/\\{[\\s\\S]*\\}/); c = JSON.parse(m[0]); } catch (e) {}\nconst q = d.quake ? d.quake.mag : '?', kp = d.sky ? d.sky.kp : '?';\nconst F = {\n  de: { earth: `Die Erde regte sich mit Stärke ${q}: Bewegung liegt in der Luft.`, sky: `Kp ${kp}: Die Sonne flüstert, du sollst mutig sein.`,\n        wanderer: `Die ISS zieht über ${d.wanderer.place}: Der Blick von oben hilft.`,\n        prophecy: `Die Erde bebt leise, die Sonne summt mit Kp ${kp}, und der ${d.moon.name} schaut zu. Alles deutet auf eine Überraschung hin, die kleiner ist als befürchtet und schöner als erhofft.`,\n        advice: 'Mach heute eine Sache zu Ende, die du lange schiebst.' },\n  en: { earth: `The earth stirred at magnitude ${q}: something is about to move.`, sky: `Kp ${kp}: the Sun whispers that you should be bold.`,\n        wanderer: `The ISS drifts over ${d.wanderer.place}: look at it from above.`,\n        prophecy: `The earth trembles softly, the Sun hums at Kp ${kp}, and the ${d.moon.name} is watching. Everything points to a surprise that is smaller than you feared and nicer than you hoped.`,\n        advice: 'Finish one thing today that you keep putting off.' },\n  it: { earth: `La terra si è mossa con magnitudo ${q}: qualcosa sta per cambiare.`, sky: `Kp ${kp}: il Sole sussurra di essere audace.`,\n        wanderer: `La ISS passa sopra ${d.wanderer.place}: guarda le cose dall'alto.`,\n        prophecy: `La terra trema piano, il Sole ronza a Kp ${kp} e la ${d.moon.name} osserva. Tutto indica una sorpresa più piccola del temuto e più bella dello sperato.`,\n        advice: 'Oggi chiudi una cosa che rimandi da tempo.' } }[d.lang];\nif (!c || !c.prophecy) c = F;\nconst cut = (v, n, fb) => String(v || fb).replace(/\\s+/g, ' ').trim().slice(0, n);\nconst { body, ...card } = d;\nreturn [{ json: { ...card, earth: cut(c.earth, 120, F.earth), sky_text: cut(c.sky, 120, F.sky), wanderer_text: cut(c.wanderer, 120, F.wanderer),\n  prophecy: cut(c.prophecy, 420, F.prophecy), advice: cut(c.advice, 110, F.advice) } }];"
   },
   "id": "c2",
   "name": "Prophezeiung prüfen",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1380,
    220
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://YOUR-SITE.example/oracle-ingest.php",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "X-Oracle-Token",
       "value": "YOUR-SECRET-TOKEN"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify($json) }}",
    "options": {
     "timeout": 60000
    }
   },
   "id": "p1",
   "name": "Karten an die Seite",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1600,
    220
   ]
  }
 ],
 "connections": {
  "Besucher-Knopf (Webhook)": {
   "main": [
    [
     {
      "node": "Erde: Erdbeben (USGS)",
      "type": "main",
      "index": 0
     },
     {
      "node": "Himmel: Sonnensturm (NOAA)",
      "type": "main",
      "index": 0
     },
     {
      "node": "Wanderer: Wo ist die ISS?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Erde: Erdbeben (USGS)": {
   "main": [
    [
     {
      "node": "Alle Omen sammeln",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Himmel: Sonnensturm (NOAA)": {
   "main": [
    [
     {
      "node": "Alle Omen sammeln",
      "type": "main",
      "index": 1
     }
    ]
   ]
  },
  "Wanderer: Wo ist die ISS?": {
   "main": [
    [
     {
      "node": "Wanderer: Was liegt darunter?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Wanderer: Was liegt darunter?": {
   "main": [
    [
     {
      "node": "Alle Omen sammeln",
      "type": "main",
      "index": 2
     }
    ]
   ]
  },
  "Alle Omen sammeln": {
   "main": [
    [
     {
      "node": "Omen deuten",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Omen deuten": {
   "main": [
    [
     {
      "node": "Das Orakel spricht (KI)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Das Orakel spricht (KI)": {
   "main": [
    [
     {
      "node": "Prophezeiung prüfen",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Prophezeiung prüfen": {
   "main": [
    [
     {
      "node": "Karten an die Seite",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "active": false,
 "settings": {
  "executionOrder": "v1",
  "timezone": "Europe/Berlin",
  "saveDataSuccessExecution": "none",
  "saveDataErrorExecution": "all"
 },
 "pinData": {}
}