const fs = require('fs'); const path = require('path'); async function fetchWithRetry(url, options = {}, retries = 3, backoff = 2000) { for (let i = 0; i < retries; i++) { try { const response = await fetch(url, options); if (response.ok) return response; if (response.status >= 500) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response; } catch (err) { if (i === retries - 1) throw err; console.warn(`Fetch to ${url} failed (attempt ${i + 1}/${retries}): ${err.message}. Retrying in ${backoff}ms...`); await new Promise(resolve => setTimeout(resolve, backoff)); } } } async function checkStatus() { try { // 1. Fetch status page HTML const htmlResponse = await fetchWithRetry('https://uptime.ginnoir.com/status/default'); if (!htmlResponse.ok) { throw new Error(`Failed to fetch status page: ${htmlResponse.statusText}`); } const html = await htmlResponse.text(); // Extract window.preloadData = ...; const preloadRegex = /window\.preloadData\s*=\s*({.*?});/s; const match = html.match(preloadRegex); if (!match) { throw new Error("Could not find window.preloadData in HTML"); } let preloadData; try { preloadData = new Function(`return ${match[1]};`)(); } catch (e) { throw new Error(`Failed to parse preloadData: ${e.message}`); } const monitors = []; if (preloadData && preloadData.publicGroupList) { for (const group of preloadData.publicGroupList) { if (group.monitorList) { monitors.push(...group.monitorList); } } } // 2. Fetch heartbeat JSON const heartbeatResponse = await fetchWithRetry('https://uptime.ginnoir.com/api/status-page/heartbeat/default'); if (!heartbeatResponse.ok) { throw new Error(`Failed to fetch heartbeat: ${heartbeatResponse.statusText}`); } const heartbeats = await heartbeatResponse.json(); // 3. Map status and print summary const monitorMap = {}; for (const monitor of monitors) { monitorMap[monitor.id] = { name: monitor.name, type: monitor.type, status: 'UNKNOWN', ping: null, lastCheck: null, msg: '' }; } const heartbeatList = heartbeats.heartbeatList || {}; for (const id in heartbeatList) { const list = heartbeatList[id]; if (list && list.length > 0) { const latest = list[list.length - 1]; if (monitorMap[id]) { monitorMap[id].status = latest.status === 1 ? 'UP' : 'DOWN'; monitorMap[id].ping = latest.ping; monitorMap[id].lastCheck = latest.time; monitorMap[id].msg = latest.msg || ''; } } } // Generate summary const monitorValues = Object.values(monitorMap); const total = monitorValues.length; const up = monitorValues.filter(m => m.status === 'UP').length; const down = monitorValues.filter(m => m.status === 'DOWN').length; const unknown = monitorValues.filter(m => m.status === 'UNKNOWN').length; let summaryMd = `### Uptime Status Summary (Checked at ${new Date().toLocaleString()})\n\n`; if (down > 0) { summaryMd += `⚠️ **Status: Degraded (${down}/${total} services DOWN)**\n\n`; } else if (up === total) { summaryMd += `✅ **Status: Healthy (All ${total} services UP)**\n\n`; } else { summaryMd += `ℹ️ **Status: Mixed (UP: ${up}, DOWN: ${down}, UNKNOWN: ${unknown})**\n\n`; } if (down > 0) { summaryMd += `#### 🚨 DOWN Services:\n`; monitorValues.filter(m => m.status === 'DOWN').forEach(m => { summaryMd += `- **${m.name}** (${m.type}) - ${m.msg || 'No message'} (Last checked: ${m.lastCheck})\n`; }); summaryMd += `\n`; } summaryMd += `#### 📋 Service Statuses:\n`; summaryMd += `| Service | Status | Latency (ms) | Last Check |\n`; summaryMd += `| :--- | :---: | :---: | :--- |\n`; // Sort services by name monitorValues.sort((a, b) => a.name.localeCompare(b.name)); for (const m of monitorValues) { const statusIcon = m.status === 'UP' ? '🟢 UP' : m.status === 'DOWN' ? '🔴 DOWN' : '⚪ UNKNOWN'; const pingText = m.ping !== null ? `${m.ping} ms` : 'N/A'; summaryMd += `| ${m.name} | ${statusIcon} | ${pingText} | ${m.lastCheck || 'N/A'} |\n`; } // Output to stdout console.log(summaryMd); // 1. Write the latest report to Uptime Status Report.md const reportPath = 'c:/Users/MattC/Documents/Obsidian Vault/Homelab/Uptime Status Report.md'; const reportContent = `--- project: Homelab type: status status: current tags: [monitoring, uptime] updated: ${new Date().toISOString().split('T')[0]} --- # Live Uptime Status Report ${summaryMd} `; fs.writeFileSync(reportPath, reportContent, 'utf8'); console.log(`Wrote status report to ${reportPath}`); // 2. Append to today's session log const todayStr = new Date().toISOString().split('T')[0]; const sessionLogDir = 'c:/Users/MattC/Documents/Obsidian Vault/Homelab/Sessions'; if (!fs.existsSync(sessionLogDir)) { fs.mkdirSync(sessionLogDir, { recursive: true }); } const sessionLogPath = path.join(sessionLogDir, `${todayStr}-uptime-status-monitoring-schedule.md`); let sessionContent = ''; if (!fs.existsSync(sessionLogPath)) { sessionContent = `# Session Log — ${todayStr} — Uptime Status Monitoring Schedule ## What was done - Automatically checked uptime status page and updated reports. ## Daily Log of Checks `; } else { sessionContent = fs.readFileSync(sessionLogPath, 'utf8'); } const timeStr = new Date().toLocaleTimeString(); const statusText = down > 0 ? `🚨 DEGRADED (${down}/${total} services DOWN)` : `✅ Healthy (${total}/${total} services UP)`; const logEntry = `\n### Check at ${timeStr}\n- **Status**: ${statusText}\n`; fs.writeFileSync(sessionLogPath, sessionContent + logEntry, 'utf8'); console.log(`Appended check entry to ${sessionLogPath}`); } catch (error) { console.error(`Error checking status: ${error.message}`); process.exit(1); } } checkStatus();