112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
/**
|
|
* 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/<pubkey>). */
|
|
export const CVM_SERVER_PUBKEY =
|
|
'940bf0e4fbc64b6c0f09d48c3264723c524a31b5ad0d1aaec361263731689d95';
|
|
|
|
/** The relay the CVM server announces on. */
|
|
export const CVM_RELAY = 'wss://relay.contextvm.org';
|
|
|
|
/**
|
|
* Freshness threshold. The server re-publishes a heartbeat announcement every
|
|
* ~15 min (`CVM_HEARTBEAT_MS`), so allow several missed beats before calling it
|
|
* stale — otherwise a healthy server reads "stale" between restarts.
|
|
*/
|
|
const STALE_AFTER_MS = 45 * 60 * 1000; // 45 minutes
|
|
|
|
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<CVMHealthResult>(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 };
|
|
}
|