diff --git a/manifest.yaml b/manifest.yaml index 475874a..5a03332 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -52,6 +52,18 @@ health-checks: name: "Web Dashboard" success-message: "Kamado dashboard is reachable." type: script + ckpool: + name: "Stratum Server" + success-message: "Stratum server is accepting connections." + type: script + bitcoin: + name: "Bitcoin Core RPC" + success-message: "Connected to Bitcoin Core." + type: script + submit-gap: + name: "Block Submission" + success-message: "All block submissions confirmed." + type: script config: get: diff --git a/scripts/procedures/healthChecks.ts b/scripts/procedures/healthChecks.ts index 54eabe5..b24c093 100644 --- a/scripts/procedures/healthChecks.ts +++ b/scripts/procedures/healthChecks.ts @@ -1,10 +1,75 @@ import { healthUtil, types as T } from "../deps.ts"; +const API_BASE = "http://kamado-pool.embassy:8080"; + +interface HealthPayload { + ok: boolean; + ckpool: boolean; + bitcoin: boolean; + submit_gap: number; + zmq_stale: boolean; + last_error?: string; +} + +async function fetchHealth(): Promise { + try { + const res = await fetch(`${API_BASE}/api/health`, { + signal: AbortSignal.timeout(5_000), + }); + return await res.json() as HealthPayload; + } catch { + return null; + } +} + +// Returns a "starting" result for the first 20 s after launch, then +// "failing" so a crashed service doesn't look like it's perpetually booting. +function apiUnreachable(duration: number, label: string): T.HealthProceduralResult { + if (duration < 20_000) { + return { status: "starting", message: `${label} is starting…` }; + } + return { status: "failing", message: "Kamado API is unreachable — service may be down." }; +} + export const health: T.ExpectedExports.health = { + // Overall reachability: uses the SDK helper so StartOS marks the + // service degraded (503) whenever ckpool or bitcoind is down. "web": async (effects, duration) => { - return healthUtil.checkWebUrl("http://kamado-pool.embassy:8080/api/health")( - effects, - duration, - ); + return healthUtil.checkWebUrl(`${API_BASE}/api/health`)(effects, duration); + }, + + // Is the stratum server up and accepting miner connections? + "ckpool": async (_effects, duration) => { + const h = await fetchHealth(); + if (!h) return apiUnreachable(duration, "Stratum server"); + if (h.ckpool) return { status: "passing", message: "Stratum server is running." }; + const detail = h.last_error ? ` (${h.last_error})` : ""; + return { status: "failing", message: `Stratum server is not responding${detail}.` }; + }, + + // Can we reach Bitcoin Core via RPC? (Required for block templates and + // block submission — if this is down, mining produces no valid work.) + "bitcoin": async (_effects, duration) => { + const h = await fetchHealth(); + if (!h) return apiUnreachable(duration, "Bitcoin Core connection"); + if (h.bitcoin) return { status: "passing", message: "Connected to Bitcoin Core." }; + const detail = h.last_error ? ` (${h.last_error})` : ""; + return { status: "failing", message: `Bitcoin Core RPC is unreachable${detail}.` }; + }, + + // Did every block submission get confirmed by bitcoind? A non-zero gap + // means at least one "Possible block solve" never got a "Solved and + // confirmed" response — the reward may have been lost or delayed. + "submit-gap": async (_effects, duration) => { + const h = await fetchHealth(); + if (!h) return apiUnreachable(duration, "Block submission check"); + const gap = h.submit_gap ?? 0; + if (gap === 0) return { status: "passing", message: "All block submissions confirmed." }; + return { + status: "failing", + message: + `${gap} block${gap === 1 ? "" : "s"} submitted to bitcoind but not confirmed — ` + + `check Bitcoin Core logs for rejected or missing submissions.`, + }; }, };