The chat's data layer (useCVMClient) queries Nostr relays directly from the browser — the CVM/ContextVM server round-trip was removed earlier for speed. But the UI and system prompt still claimed data came "from the CVM server", which is no longer true. Aligned every user-facing claim with reality: - System prompt: reframed "CVM Tools / MUST route through CVM" as "Live data tools that query the Nostr relays directly". Tool-calling behaviour unchanged. - Streaming status: "Querying CVM for X" → "Searching the relays for X". - Tool-failure message: no longer blames a non-existent "CVM server"; now correctly frames it as relays being slow, with a retry nudge. - Empty results: stop asserting "not added yet" as fact — an empty result can also mean slow relays or a misspelled place. Now offers retry + add-a-spot, and names the place searched. Applied to the local formatter, the system prompt rules, and the second-pass AI instruction. - Chat footer: "Powered by ContextVM & Nostr Relays Data" → "Live data from Nostr relays" (links the primary community relay). - About page credit: "AI powered by ContextVM" → "Trust & crew chat via ContextVM" (its real role: Relatr trust scores + encrypted crew chat). - Owner CVM health panel: clarified it probes the standalone MCP server and that the in-app chat does not depend on it. - Updated stale internal comments in SasquatchGuide + CreateSpotPage. ContextVM references that ARE accurate (Relatr trust scores, crew-chat Cordn coordinator) were left intact. Build clean, no console errors. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
193 lines
7.2 KiB
TypeScript
193 lines
7.2 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">
|
||
Heartbeat probe for the standalone ContextVM MCP server. Note: the
|
||
in-app Sasquatch chat reads Nostr relays directly and does not depend
|
||
on this 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>
|
||
);
|
||
}
|