Probes the ContextVM relay for the CVM server's own events and reports online/stale/offline with a last-seen timestamp. Gated to the two platform owner pubkeys (renders null for everyone else). Notes that Sasquatch's data layer queries relays directly and is unaffected by the server being down. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
108 lines
3.6 KiB
TypeScript
108 lines
3.6 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 =
|
|
'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<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 };
|
|
}
|