{
  "name": "nola \u2014 weekly health digest",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "cronExpression": "0 14 * * 1"
        }
      },
      "id": "schedule-trigger",
      "name": "Monday 6AM Pacific",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Collect health summary from all Netdata hosts\nconst hostString = process.env.NETDATA_HOSTS || '';\nconst hosts = {};\nhostString.split(',').forEach(h => {\n  const colonIdx = h.indexOf(':http');\n  if (colonIdx > 0) {\n    hosts[h.slice(0, colonIdx).trim()] = 'http' + h.slice(colonIdx + 1).trim();\n  }\n});\n\nconst lines = ['\ud83d\udcca **NOLA Weekly Health Digest**', ''];\n\nawait Promise.all(Object.entries(hosts).map(async ([name, baseUrl]) => {\n  try {\n    const resp = await $helpers.httpRequest({ method: 'GET', url: `${baseUrl}/api/v1/allmetrics?format=json`, timeout: 10000 });\n    const d = typeof resp === 'string' ? JSON.parse(resp) : resp;\n\n    const cpuIdle = d?.['system.cpu']?.dimensions?.idle?.value;\n    const cpuUsed = cpuIdle !== undefined ? (100 - cpuIdle).toFixed(1) + '%' : 'N/A';\n\n    const ramUsed = d?.['system.ram']?.dimensions?.used?.value;\n    const ramFree = d?.['system.ram']?.dimensions?.free?.value;\n    const ramBuff = (d?.['system.ram']?.dimensions?.buffers?.value || 0) + (d?.['system.ram']?.dimensions?.cached?.value || 0);\n    let ramStr = 'N/A';\n    if (ramUsed !== undefined && ramFree !== undefined) {\n      const total = ramUsed + ramFree + ramBuff;\n      const pct   = total > 0 ? ((ramUsed / total) * 100).toFixed(1) : '0';\n      ramStr = `${pct}%`;\n    }\n\n    const uptime  = d?.['system.uptime']?.dimensions?.uptime?.value;\n    const uptimeStr = uptime !== undefined\n      ? uptime > 86400 ? `${Math.floor(uptime / 86400)}d ${Math.floor((uptime % 86400) / 3600)}h`\n        : `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`\n      : 'N/A';\n\n    lines.push(`**${name}**: CPU ${cpuUsed} | RAM ${ramStr} | Uptime ${uptimeStr}`);\n  } catch (err) {\n    lines.push(`**${name}**: \u274c unreachable`);\n  }\n}));\n\n// UPS\ntry {\n  const net = require('net');\n  const UPS_HOST = process.env.UPS_HOST;\n  const UPS_PORT = parseInt(process.env.UPS_PORT || '3551');\n  if (UPS_HOST) {\n    const upsRaw = await new Promise((resolve, reject) => {\n      const client = net.createConnection(UPS_PORT, UPS_HOST, () => {\n        const cmd = Buffer.alloc(8);\n        cmd.writeUInt16BE(6, 0);\n        cmd.write('status', 2);\n        client.write(cmd);\n      });\n      let data = Buffer.alloc(0);\n      client.on('data', c => { data = Buffer.concat([data, c]); });\n      client.on('end', () => resolve(data.toString('ascii')));\n      client.on('error', reject);\n      setTimeout(() => { client.destroy(); reject(new Error('timeout')); }, 5000);\n    });\n    const kv = {};\n    upsRaw.split('\\n').forEach(line => {\n      const i = line.indexOf(':');\n      if (i > 0) kv[line.slice(0, i).trim()] = line.slice(i + 1).trim();\n    });\n    const model    = kv['MODEL']    || 'UPS';\n    const status   = kv['STATUS']   || 'UNKNOWN';\n    const bcharge  = kv['BCHARGE']  || '?';\n    const loadpct  = kv['LOADPCT']  || '?';\n    const timeleft = kv['TIMELEFT'] || '?';\n    lines.push('');\n    lines.push(`\ud83d\udd0b **UPS** (${model}): ${status} | Battery ${bcharge} | Load ${loadpct} | Runtime ${timeleft}`);\n  }\n} catch (err) {\n  lines.push(`\ud83d\udd0b **UPS**: check failed \u2014 ${err.message}`);\n}\n\n// Prometheus scraper health\ntry {\n  const promUrl = process.env.PROMETHEUS_URL;\n  if (promUrl) {\n    const resp = await $helpers.httpRequest({ method: 'GET', url: `${promUrl}/api/v1/query?query=up`, timeout: 8000 });\n    const data  = typeof resp === 'string' ? JSON.parse(resp) : resp;\n    const results = data?.data?.result || [];\n    const up   = results.filter(r => r.value?.[1] === '1').length;\n    const down = results.filter(r => r.value?.[1] === '0').length;\n    lines.push('');\n    lines.push(`\ud83d\udce1 **Prometheus scrapers**: ${up} up, ${down} down`);\n    if (down > 0) {\n      results.filter(r => r.value?.[1] === '0').forEach(r => {\n        lines.push(`   \u274c ${r.metric?.instance || r.metric?.job}`);\n      });\n    }\n  }\n} catch (err) {\n  lines.push(`\ud83d\udce1 **Prometheus**: check failed`);\n}\n\nlines.push('');\nlines.push(`_Generated by NOLA \u2014 ${new Date().toUTCString()}_`);\n\nreturn [{ json: { rawDigest: lines.join('\\n') } }];"
      },
      "id": "build-digest",
      "name": "Build Health Digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Ask Ollama (knox.universe) to write a brief narrative for the weekly digest.\n// Falls back gracefully if Ollama is unavailable.\nconst rawDigest = $input.first().json.rawDigest;\n\nconst ollamaUrl = process.env.OLLAMA_URL_KNOX || 'http://knox.universe:11434';\nconst model = process.env.OLLAMA_MODEL || 'llama3.2:3b';\n\nconst prompt = `You are NOLA, a homelab AI butler. Based on this weekly health report, write a single short paragraph (2-3 sentences) with your overall assessment of the lab's health this week. Be conversational but technical. Don't repeat all the numbers \u2014 give your read on whether things look good, concerning, or need attention.\\n\\nReport:\\n${rawDigest}`;\n\nlet narrative = null;\ntry {\n  const resp = await $helpers.httpRequest({\n    method: 'POST',\n    url: `${ollamaUrl}/api/generate`,\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ model, prompt, stream: false }),\n    timeout: 45000\n  });\n  const data = typeof resp === 'string' ? JSON.parse(resp) : resp;\n  narrative = data?.response?.trim() || null;\n} catch (err) {\n  // Ollama unavailable \u2014 post digest without narrative\n}\n\nconst message = narrative\n  ? `${rawDigest}\\n\\n\ud83d\udcac **NOLA's Take:** ${narrative}`\n  : rawDigest;\n\nreturn [{ json: { message } }];"
      },
      "id": "ollama-narrate",
      "name": "Narrate with Ollama",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.DISCORD_WEBHOOK_URL }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"content\": \"{{ $json.message }}\" }",
        "options": {}
      },
      "id": "post-discord",
      "name": "Post to Discord",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        880,
        300
      ]
    }
  ],
  "connections": {
    "Monday 6AM Pacific": {
      "main": [
        [
          {
            "node": "Build Health Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Health Digest": {
      "main": [
        [
          {
            "node": "Narrate with Ollama",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Narrate with Ollama": {
      "main": [
        [
          {
            "node": "Post to Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    "nola",
    "monitor"
  ]
}