/** * useCVMServerHealth * ───────────────────────────────────────────────────────────────────────────── * Owner-only health probe for the SkateSpots ContextVM (CVM) MCP server. * * A ContextVM server lives as a Nostr identity that publishes a server * announcement and listens for encrypted MCP requests on the ContextVM relay. * The most reliable "is it alive?" signal we can read from the browser is the * server's own events on that relay: the announcement (kind 31990) plus any * newer heartbeat/activity. We report status + how long ago the server was * last seen. * * NOTE: the user-facing Sasquatch data layer (`useCVMClient`) queries the data * relays DIRECTLY and does NOT depend on this server being up. This probe is * purely an operational status check for platform owners. */ import { useCallback, useState } from 'react'; import { useNostr } from '@nostrify/react'; /** The SkateSpots CVM server identity (see contextvm.org/s/). */ export const CVM_SERVER_PUBKEY = 'e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530'; /** The relay the CVM server announces on. */ export const CVM_RELAY = 'wss://relay.contextvm.org'; /** Freshness threshold: if the newest event is older than this, call it stale. */ const STALE_AFTER_MS = 60 * 60 * 1000; // 1 hour export type CVMHealthStatus = 'idle' | 'checking' | 'online' | 'stale' | 'offline' | 'error'; export interface CVMHealthResult { status: CVMHealthStatus; /** Unix seconds of the newest event authored by the server, if any. */ lastSeen: number | null; /** Number of server-authored events found on the probe. */ eventCount: number; /** Human-readable error, when status === 'error'. */ error: string | null; /** When the probe last ran (ms epoch). */ checkedAt: number | null; } const INITIAL: CVMHealthResult = { status: 'idle', lastSeen: null, eventCount: 0, error: null, checkedAt: null, }; export function useCVMServerHealth() { const { nostr } = useNostr(); const [result, setResult] = useState(INITIAL); const check = useCallback(async () => { setResult((r) => ({ ...r, status: 'checking', error: null })); try { // Probe the ContextVM relay directly for anything the server has authored. // We ask for a small window across the kinds a CVM server emits: // 31990 (handler/announcement), plus recent activity of any kind. const relay = nostr.relay(CVM_RELAY); const signal = AbortSignal.timeout(8000); const events = await relay.query( [{ authors: [CVM_SERVER_PUBKEY], limit: 25 }], { signal }, ); if (!events.length) { setResult({ status: 'offline', lastSeen: null, eventCount: 0, error: null, checkedAt: Date.now(), }); return; } const lastSeen = Math.max(...events.map((e) => e.created_at)); const ageMs = Date.now() - lastSeen * 1000; const status: CVMHealthStatus = ageMs <= STALE_AFTER_MS ? 'online' : 'stale'; setResult({ status, lastSeen, eventCount: events.length, error: null, checkedAt: Date.now(), }); } catch (err) { setResult({ status: 'error', lastSeen: null, eventCount: 0, error: err instanceof Error ? err.message : 'Could not reach the ContextVM relay', checkedAt: Date.now(), }); } }, [nostr]); const reset = useCallback(() => setResult(INITIAL), []); return { ...result, check, reset }; }