SkateSpots/src/components/CVMHealthCheck.tsx
shakespeare.diy e6517b5c70 Add owner-only ContextVM server health check (Settings > Advanced)
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>
2026-07-29 22:27:22 +07:00

191 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* CVMHealthCheck — owner-only status probe for the SkateSpots ContextVM server.
*
* Renders NOTHING unless the logged-in user is one of the two platform owners
* (PLATFORM_OWNER_PUBKEYS). Pings the ContextVM relay for the server's own
* events and reports online / stale / offline with a last-seen timestamp.
*/
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
import {
useCVMServerHealth,
CVM_SERVER_PUBKEY,
CVM_RELAY,
type CVMHealthStatus,
} from '@/hooks/useCVMServerHealth';
import { cn } from '@/lib/utils';
import {
Activity,
CheckCircle2,
XCircle,
AlertTriangle,
Loader2,
ExternalLink,
} from 'lucide-react';
function timeAgo(unixSeconds: number): string {
const s = Math.floor(Date.now() / 1000 - unixSeconds);
if (s < 60) return `${s}s ago`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
const d = Math.floor(h / 24);
return `${d}d ago`;
}
const STATUS_META: Record<
CVMHealthStatus,
{ label: string; color: string; ring: string; dot: string }
> = {
idle: { label: 'Not checked', color: 'text-gray-400', ring: 'border-white/10', dot: 'bg-gray-500' },
checking: { label: 'Checking…', color: 'text-blue-300', ring: 'border-blue-500/30', dot: 'bg-blue-400' },
online: { label: 'Online', color: 'text-green-400', ring: 'border-green-500/30', dot: 'bg-green-400' },
stale: { label: 'Stale', color: 'text-amber-400', ring: 'border-amber-500/30', dot: 'bg-amber-400' },
offline: { label: 'Offline', color: 'text-red-400', ring: 'border-red-500/30', dot: 'bg-red-500' },
error: { label: 'Unreachable', color: 'text-red-400', ring: 'border-red-500/30', dot: 'bg-red-500' },
};
export function CVMHealthCheck() {
const { user } = useCurrentUser();
const health = useCVMServerHealth();
// Owner-only gate. Never render for anyone else.
const isOwner = !!user?.pubkey && PLATFORM_OWNER_PUBKEYS.includes(user.pubkey);
if (!isOwner) return null;
const meta = STATUS_META[health.status];
const StatusIcon =
health.status === 'checking'
? Loader2
: health.status === 'online'
? CheckCircle2
: health.status === 'stale'
? AlertTriangle
: health.status === 'idle'
? Activity
: XCircle;
return (
<div className={cn('rounded-2xl border overflow-hidden bg-white/2', meta.ring)}>
<div className="px-4 py-3.5 border-b border-white/5 flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-bold text-white flex items-center gap-2">
<Activity className="h-4 w-4 text-red-400" />
ContextVM Server
<span className="text-[9px] font-black uppercase tracking-widest text-red-400/70 bg-red-500/10 px-1.5 py-0.5 rounded">
Owner
</span>
</p>
<p className="text-xs text-muted-foreground mt-0.5">
Health probe for the Sasquatch CVM data server
</p>
</div>
<span
className={cn(
'shrink-0 flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full border',
meta.ring,
meta.color,
)}
>
<span className={cn('h-1.5 w-1.5 rounded-full', meta.dot)} />
{meta.label}
</span>
</div>
<div className="px-4 py-3.5 space-y-3">
{/* Status detail line */}
<div className="flex items-start gap-2.5">
<StatusIcon
className={cn(
'h-4 w-4 mt-0.5 shrink-0',
meta.color,
health.status === 'checking' && 'animate-spin',
)}
/>
<div className="text-xs leading-relaxed">
{health.status === 'idle' && (
<span className="text-muted-foreground">
Tap “Check server” to ping the ContextVM relay for the server’s heartbeat.
</span>
)}
{health.status === 'checking' && (
<span className="text-blue-300">Querying {CVM_RELAY}…</span>
)}
{health.status === 'online' && health.lastSeen && (
<span className="text-green-400">
Server is announcing. Last event {timeAgo(health.lastSeen)}.
</span>
)}
{health.status === 'stale' && health.lastSeen && (
<span className="text-amber-400">
Server found, but last activity was {timeAgo(health.lastSeen)} — it may be
down or idle.
</span>
)}
{health.status === 'offline' && (
<span className="text-red-400">
No events from the server on the relay. It’s likely not running.
</span>
)}
{health.status === 'error' && (
<span className="text-red-400">{health.error}</span>
)}
</div>
</div>
{/* Meta grid */}
{(health.eventCount > 0 || health.checkedAt) && (
<div className="grid grid-cols-2 gap-2 text-[11px]">
<div className="rounded-lg bg-black/30 border border-white/5 px-2.5 py-2">
<p className="text-gray-500 uppercase tracking-wide font-bold text-[9px]">
Events found
</p>
<p className="text-white font-mono">{health.eventCount}</p>
</div>
<div className="rounded-lg bg-black/30 border border-white/5 px-2.5 py-2">
<p className="text-gray-500 uppercase tracking-wide font-bold text-[9px]">
Last seen
</p>
<p className="text-white font-mono">
{health.lastSeen ? timeAgo(health.lastSeen) : '—'}
</p>
</div>
</div>
)}
{/* Note — Sasquatch does NOT depend on this server. */}
<p className="text-[11px] text-muted-foreground/70 leading-relaxed">
Note: Sasquatch queries data relays directly — it keeps working even if this
server is down. This check is for operations only.
</p>
{/* Actions */}
<div className="flex items-center gap-2 pt-0.5">
<button
onClick={health.check}
disabled={health.status === 'checking'}
className="flex items-center gap-2 text-sm font-bold text-red-400 hover:text-red-300 disabled:opacity-50 transition-colors"
>
{health.status === 'checking' ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Activity className="h-4 w-4" />
)}
Check server
</button>
<a
href={`https://contextvm.org/s/${CVM_SERVER_PUBKEY}`}
target="_blank"
rel="noopener noreferrer"
className="ml-auto flex items-center gap-1.5 text-xs font-semibold text-gray-400 hover:text-white transition-colors"
>
Directory <ExternalLink className="h-3.5 w-3.5" />
</a>
</div>
</div>
</div>
);
}