Files
Admin_dash/components/widgets/GlobeCard.tsx
Shivam Patel d5ab2db926 Wire up visitor metrics from webserver DB and fix uptime monitoring
- Read visitors from /server_storage/visitors.db (webserver's DB) instead of
  admin dash's own table; geoip lookups at query time for globe markers
- Globe card now shows 24h, 7d, and all-time unique visitor counts
- Uptime monitor: Nextcloud via host.docker.internal for Docker networking,
  Website and Gitea monitored on public domains
- UptimeCard uses real hourly history bars instead of Math.random() mock
- docker-compose: mount /server_storage:ro, add extra_hosts for Linux compat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 00:50:40 -05:00

101 lines
3.5 KiB
TypeScript

'use client';
import createGlobe from 'cobe';
import { useEffect, useRef, useState } from 'react';
import { Globe } from 'lucide-react';
export function GlobeCard() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [visitorStats, setVisitorStats] = useState({
total: 0,
active24h: 0,
active7d: 0,
locations: [],
});
useEffect(() => {
async function fetchVisitors() {
try {
const res = await fetch('/api/visitors');
const data = await res.json();
setVisitorStats({
total: data.totalVisitors || 0,
active24h: data.active24h || 0,
active7d: data.active7d || 0,
locations: data.locations || [],
});
} catch (e) {
console.error('Failed to load visitors', e);
}
}
fetchVisitors();
const interval = setInterval(fetchVisitors, 30000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
let phi = 0;
if (!canvasRef.current) return;
const globe = createGlobe(canvasRef.current, {
devicePixelRatio: 2,
width: 600 * 2,
height: 600 * 2,
phi: 0,
theta: 0,
dark: 1,
diffuse: 1.2,
mapSamples: 16000,
mapBrightness: 6,
baseColor: [0.3, 0.3, 0.3],
markerColor: [0.1, 0.8, 1],
glowColor: [0.1, 0.1, 0.2],
markers: visitorStats.locations,
onRender: (state) => {
state.phi = phi;
phi += 0.01;
},
});
return () => {
globe.destroy();
};
}, [visitorStats.locations]);
return (
<div className="col-span-1 md:col-span-2 lg:col-span-2 row-span-2 bg-neutral-900 border border-neutral-800 rounded-xl relative overflow-hidden group hover:border-neutral-700 transition-colors">
<div className="absolute top-6 left-6 z-10 pointer-events-none">
<div className="flex items-center gap-2 text-neutral-400">
<Globe size={18} />
<span className="text-sm font-medium">Visitor Map</span>
</div>
<div className="mt-4 space-y-3">
<div>
<span className="text-2xl font-bold text-white tracking-tight block">{visitorStats.active24h}</span>
<span className="text-xs text-neutral-500 font-mono">LAST 24H</span>
</div>
<div>
<span className="text-lg font-bold text-neutral-300 block">{visitorStats.active7d}</span>
<span className="text-xs text-neutral-500 font-mono">LAST 7D</span>
</div>
</div>
</div>
<div className="absolute top-6 right-6 z-10 text-right pointer-events-none">
<div>
<span className="text-xl font-bold text-neutral-300 block">{visitorStats.total}</span>
<span className="text-xs text-neutral-500 font-mono">ALL TIME</span>
</div>
</div>
<div className="absolute inset-0 flex items-center justify-center opacity-80 mt-10">
<canvas
ref={canvasRef}
style={{ width: 600, height: 600, maxWidth: '100%', aspectRatio: 1 }}
/>
</div>
</div>
);
}