168 lines
6.9 KiB
TypeScript
168 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import React from 'react';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { useAppStore } from '@/store/app-store';
|
|
import { formatTimestamp } from '@/lib/utils';
|
|
import { ChevronRight } from 'lucide-react';
|
|
|
|
export function NexusSyncMonitor() {
|
|
const { syncEvents, syncStates } = useAppStore();
|
|
const [expandedItems, setExpandedItems] = React.useState<Set<string>>(new Set(['players']));
|
|
|
|
const toggleExpand = (id: string) => {
|
|
setExpandedItems(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex bg-background">
|
|
{/* Left: State Tree */}
|
|
<div className="w-1/2 border-r border-gray-800 flex flex-col">
|
|
<div className="px-4 py-3 border-b border-gray-800">
|
|
<h3 className="font-semibold text-white">State Tree</h3>
|
|
</div>
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-4 font-mono text-sm space-y-1">
|
|
<div className="text-primary">└─ GameState</div>
|
|
|
|
<div>
|
|
<button
|
|
onClick={() => toggleExpand('players')}
|
|
className="flex items-center gap-1 text-gray-300 hover:text-white w-full"
|
|
>
|
|
<ChevronRight className={`w-4 h-4 transition-transform ${expandedItems.has('players') ? 'rotate-90' : ''}`} />
|
|
<span>├─ Players (3 connected)</span>
|
|
</button>
|
|
{expandedItems.has('players') && (
|
|
<div className="ml-8 space-y-1 mt-1">
|
|
<div className="text-gray-400">├─ Player_001</div>
|
|
<div className="ml-4 text-gray-500">├─ position: {JSON.stringify({x: 120, y: 85, z: 0})}</div>
|
|
<div className="ml-4 text-gray-500">├─ health: 100</div>
|
|
<div className="ml-4 text-gray-500">└─ inventory: [item1, item2]</div>
|
|
<div className="text-gray-400">├─ Player_002</div>
|
|
<div className="text-gray-400">└─ Player_003</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<button
|
|
onClick={() => toggleExpand('world')}
|
|
className="flex items-center gap-1 text-gray-300 hover:text-white w-full"
|
|
>
|
|
<ChevronRight className={`w-4 h-4 transition-transform ${expandedItems.has('world') ? 'rotate-90' : ''}`} />
|
|
<span>├─ World</span>
|
|
</button>
|
|
{expandedItems.has('world') && (
|
|
<div className="ml-8 space-y-1 mt-1">
|
|
<div className="text-gray-500">├─ objects: [...]</div>
|
|
<div className="text-gray-500">└─ weather: "sunny"</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<button
|
|
onClick={() => toggleExpand('matchmaking')}
|
|
className="flex items-center gap-1 text-gray-300 hover:text-white w-full"
|
|
>
|
|
<ChevronRight className={`w-4 h-4 transition-transform ${expandedItems.has('matchmaking') ? 'rotate-90' : ''}`} />
|
|
<span>└─ Matchmaking</span>
|
|
</button>
|
|
{expandedItems.has('matchmaking') && (
|
|
<div className="ml-8 space-y-1 mt-1">
|
|
<div className="text-gray-500">└─ queue: []</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
|
|
{/* Right: Activity Feed */}
|
|
<div className="w-1/2 flex flex-col">
|
|
<div className="px-4 py-3 border-b border-gray-800">
|
|
<h3 className="font-semibold text-white">Sync Activity Feed</h3>
|
|
</div>
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-4 font-mono text-xs space-y-2">
|
|
{/* Mock events */}
|
|
<div className="flex gap-2 text-blue-400">
|
|
<span className="text-gray-500">14:30:45.123</span>
|
|
<span>[Roblox→All] Player_001.position updated</span>
|
|
</div>
|
|
<div className="flex gap-2 text-gray-400">
|
|
<span className="text-gray-500">14:30:45.124</span>
|
|
<span>[Web] Ack received (12ms latency)</span>
|
|
</div>
|
|
<div className="flex gap-2 text-gray-400">
|
|
<span className="text-gray-500">14:30:45.126</span>
|
|
<span>[Mobile] Ack received (14ms latency)</span>
|
|
</div>
|
|
<div className="flex gap-2 text-gray-400">
|
|
<span className="text-gray-500">14:30:45.140</span>
|
|
<span>[Desktop] Ack received (28ms latency)</span>
|
|
</div>
|
|
<div className="flex gap-2 text-blue-400">
|
|
<span className="text-gray-500">14:30:46.001</span>
|
|
<span>[Web→All] Player_002.health changed: 100→95</span>
|
|
</div>
|
|
<div className="flex gap-2 text-yellow-400">
|
|
<span className="text-gray-500">14:30:46.015</span>
|
|
<span>[Conflict] Player_001.inventory - resolving...</span>
|
|
</div>
|
|
<div className="flex gap-2 text-green-400">
|
|
<span className="text-gray-500">14:30:46.020</span>
|
|
<span>[Resolved] Server authority applied</span>
|
|
</div>
|
|
|
|
{syncEvents.map((event: any) => (
|
|
<div
|
|
key={event.id}
|
|
className={`flex gap-2 ${
|
|
event.type === 'sync' ? 'text-blue-400' :
|
|
event.type === 'ack' ? 'text-gray-400' :
|
|
event.type === 'conflict' ? 'text-yellow-400' :
|
|
'text-green-400'
|
|
}`}
|
|
>
|
|
<span className="text-gray-500">{formatTimestamp(event.timestamp)}</span>
|
|
<span>[{event.source}→{event.target}] {event.variable} {event.type}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* Bottom Stats */}
|
|
<div className="px-4 py-3 border-t border-gray-800 bg-surface">
|
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
|
<div>
|
|
<div className="text-gray-400">Total syncs</div>
|
|
<div className="text-white font-semibold text-lg">1,247</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-400">Conflicts</div>
|
|
<div className="text-yellow-400 font-semibold text-lg">3</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-400">Avg latency</div>
|
|
<div className="text-green-400 font-semibold text-lg">18ms</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-400">Bandwidth</div>
|
|
<div className="text-cyan-400 font-semibold text-lg">2.3 KB/s</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|