Files
homelabstack/scripts/check_uptime_to_obsidian.js
ginnoirandClaude Sonnet 5 10997e4b16 feat(scripts): add uptime-check retry + Obsidian variant, add LLM benchmark docs
check_uptime.js gets a fetchWithRetry wrapper (3 attempts, 2s backoff)
for transient failures against the status page/heartbeat API.
check_uptime_to_obsidian.js is a variant that logs results into the
Obsidian vault instead of stdout. Also adds two benchmark writeups
(gpt-oss-20b on Ollama, 73-node Ollama fleet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 02:17:53 -05:00

180 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();