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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1792dd964b
commit
10997e4b16
+19
-2
@@ -1,10 +1,27 @@
|
||||
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 fetch('https://uptime.ginnoir.com/status/default');
|
||||
const htmlResponse = await fetchWithRetry('https://uptime.ginnoir.com/status/default');
|
||||
if (!htmlResponse.ok) {
|
||||
throw new Error(`Failed to fetch status page: ${htmlResponse.statusText}`);
|
||||
}
|
||||
@@ -34,7 +51,7 @@ async function checkStatus() {
|
||||
}
|
||||
|
||||
// 2. Fetch heartbeat JSON
|
||||
const heartbeatResponse = await fetch('https://uptime.ginnoir.com/api/status-page/heartbeat/default');
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user