Files
Admin_dash/components/widgets/UptimeCard.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

96 lines
4.3 KiB
TypeScript

'use client';
import { Activity, RefreshCcw, AlertCircle } from 'lucide-react';
import { useState, useEffect } from 'react';
interface ServiceStatus {
name: string;
url: string;
status: 'up' | 'down';
latency: number;
uptime24h?: number;
uptime7d?: number;
history?: Array<{ hour: string; up: boolean }>;
}
export function UptimeCard() {
const [services, setServices] = useState<ServiceStatus[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetchStatus = async () => {
try {
const res = await fetch('/api/status');
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setServices(data);
setError(false);
} catch (e) {
console.error(e);
setError(true);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 30000);
return () => clearInterval(interval);
}, []);
return (
<div className="col-span-1 md:col-span-2 lg:col-span-2 row-span-1 bg-neutral-900 border border-neutral-800 rounded-xl p-6 flex flex-col justify-between hover:border-neutral-700 transition-colors">
<div className="flex justify-between items-start">
<div className="flex items-center gap-2 text-neutral-400">
<Activity size={18} />
<span className="text-sm font-medium">Uptime Monitor</span>
{loading && <RefreshCcw size={12} className="animate-spin ml-2 opacity-50" />}
</div>
</div>
{error ? (
<div className="flex flex-col items-center justify-center h-full text-red-400 gap-2">
<AlertCircle size={24} />
<span className="text-xs">Failed to load status</span>
</div>
) : (
<div className="space-y-4 mt-4">
{services.map((service) => (
<div key={service.name} className="flex flex-col gap-1">
<div className="flex items-center justify-between group">
<span className="text-sm text-neutral-300 font-medium group-hover:text-white transition-colors">{service.name}</span>
<div className="flex items-center gap-2">
<span className={`text-xs px-1.5 py-0.5 rounded ${service.status === 'up' ? 'bg-emerald-500/10 text-emerald-500' : 'bg-red-500/10 text-red-500'}`}>
{service.status === 'up' ? 'UP' : 'DOWN'}
</span>
<span className="text-xs text-neutral-500 font-mono w-10 text-right">{service.latency}ms</span>
</div>
</div>
<div className="flex gap-[2px] h-1.5 opacity-50">
{service.history && service.history.length > 0 ? (
service.history.slice(-24).map((h, i) => (
<div
key={i}
className={`flex-1 rounded-full ${h.up ? 'bg-emerald-500' : 'bg-red-500'}`}
title={h.hour}
/>
))
) : (
[...Array(24)].map((_, i) => (
<div key={i} className="flex-1 rounded-full bg-neutral-700" />
))
)}
</div>
<div className="flex justify-between text-[10px] text-neutral-600 font-mono">
<span>24h: {service.uptime24h ?? 100}%</span>
<span>7d: {service.uptime7d ?? 100}%</span>
</div>
</div>
))}
</div>
)}
</div>
);
}