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>
191 lines
7.1 KiB
TypeScript
191 lines
7.1 KiB
TypeScript
/**
|
||
* 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>
|
||
);
|
||
}
|