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>
This commit is contained in:
@@ -9,31 +9,53 @@ export async function GET() {
|
||||
|
||||
// Get live status (most recent log per service)
|
||||
const live = await db.all(`
|
||||
SELECT service_name as name, url, status, latency
|
||||
FROM uptime_logs
|
||||
SELECT service_name as name, url, status, latency
|
||||
FROM uptime_logs
|
||||
WHERE id IN (SELECT MAX(id) FROM uptime_logs GROUP BY service_name)
|
||||
`);
|
||||
|
||||
// Get 24h stats
|
||||
const stats24h = await db.all(`
|
||||
SELECT service_name,
|
||||
count(*) as total,
|
||||
sum(case when status = 'up' then 1 else 0 end) as up_count
|
||||
FROM uptime_logs
|
||||
SELECT service_name,
|
||||
count(*) as total,
|
||||
sum(case when status = 'up' then 1 else 0 end) as up_count
|
||||
FROM uptime_logs
|
||||
WHERE timestamp > datetime('now', '-24 hours')
|
||||
GROUP BY service_name
|
||||
`);
|
||||
|
||||
// Get 7d stats
|
||||
const stats7d = await db.all(`
|
||||
SELECT service_name,
|
||||
count(*) as total,
|
||||
sum(case when status = 'up' then 1 else 0 end) as up_count
|
||||
FROM uptime_logs
|
||||
SELECT service_name,
|
||||
count(*) as total,
|
||||
sum(case when status = 'up' then 1 else 0 end) as up_count
|
||||
FROM uptime_logs
|
||||
WHERE timestamp > datetime('now', '-7 days')
|
||||
GROUP BY service_name
|
||||
`);
|
||||
|
||||
// Get hourly history for last 24h (24 buckets per service)
|
||||
const history = await db.all(`
|
||||
SELECT service_name,
|
||||
strftime('%Y-%m-%d %H:00', timestamp) as hour,
|
||||
count(*) as total,
|
||||
sum(case when status = 'up' then 1 else 0 end) as up_count
|
||||
FROM uptime_logs
|
||||
WHERE timestamp > datetime('now', '-24 hours')
|
||||
GROUP BY service_name, hour
|
||||
ORDER BY hour ASC
|
||||
`);
|
||||
|
||||
// Group history by service
|
||||
const historyMap: Record<string, Array<{ hour: string; up: boolean }>> = {};
|
||||
for (const row of history) {
|
||||
if (!historyMap[row.service_name]) historyMap[row.service_name] = [];
|
||||
historyMap[row.service_name].push({
|
||||
hour: row.hour,
|
||||
up: row.up_count === row.total,
|
||||
});
|
||||
}
|
||||
|
||||
// Merge data
|
||||
const results = live.map(l => {
|
||||
const s24 = stats24h.find(s => s.service_name === l.name);
|
||||
@@ -43,13 +65,13 @@ export async function GET() {
|
||||
...l,
|
||||
uptime24h: s24 ? Math.round((s24.up_count / s24.total) * 100) : 100,
|
||||
uptime7d: s7d ? Math.round((s7d.up_count / s7d.total) * 100) : 100,
|
||||
history: historyMap[l.name] || [],
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json(results);
|
||||
} catch (error) {
|
||||
console.error('Uptime stats error:', error);
|
||||
// Fallback to simple check if DB fails
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,76 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDb } from '@/lib/db';
|
||||
import { getVisitorsDb } from '@/lib/visitors-db';
|
||||
import geoip from 'geoip-lite';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const db = await getVisitorsDb();
|
||||
|
||||
// Get active/recent visitors (last 24h)
|
||||
const recent = await db.all(`
|
||||
SELECT lat, lon, city, country, count(*) as count
|
||||
FROM visitors
|
||||
WHERE timestamp > datetime('now', '-24 hours')
|
||||
GROUP BY lat, lon
|
||||
`);
|
||||
if (!db) {
|
||||
return NextResponse.json({
|
||||
locations: [],
|
||||
totalVisitors: 0,
|
||||
active24h: 0,
|
||||
active7d: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Get total count
|
||||
const total = await db.get(`SELECT count(*) as count FROM visitors`);
|
||||
// Count unique visitors across time windows
|
||||
const [total, last24h, last7d] = await Promise.all([
|
||||
db.get(`SELECT COUNT(DISTINCT ip_address) as count FROM visits`),
|
||||
db.get(`SELECT COUNT(DISTINCT ip_address) as count FROM visits WHERE visited_at >= datetime('now', '-24 hours')`),
|
||||
db.get(`SELECT COUNT(DISTINCT ip_address) as count FROM visits WHERE visited_at >= datetime('now', '-7 days')`),
|
||||
]);
|
||||
|
||||
// Get unique IPs from last 7 days for globe markers
|
||||
const recentIps = await db.all(
|
||||
`SELECT ip_address, COUNT(*) as hits FROM visits WHERE visited_at >= datetime('now', '-7 days') GROUP BY ip_address`
|
||||
);
|
||||
|
||||
// Geo-locate IPs and group by location
|
||||
const locationMap = new Map<string, { lat: number; lon: number; city: string; country: string; count: number }>();
|
||||
|
||||
for (const row of recentIps) {
|
||||
const geo = geoip.lookup(row.ip_address);
|
||||
if (geo && geo.ll) {
|
||||
const key = `${geo.ll[0]},${geo.ll[1]}`;
|
||||
const existing = locationMap.get(key);
|
||||
if (existing) {
|
||||
existing.count += row.hits;
|
||||
} else {
|
||||
locationMap.set(key, {
|
||||
lat: geo.ll[0],
|
||||
lon: geo.ll[1],
|
||||
city: geo.city || 'Unknown',
|
||||
country: geo.country || 'Unknown',
|
||||
count: row.hits,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const locations = Array.from(locationMap.values()).map(loc => ({
|
||||
location: [loc.lat, loc.lon],
|
||||
size: Math.min(0.1, 0.05 + loc.count * 0.005),
|
||||
city: loc.city,
|
||||
country: loc.country,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
locations: recent.map(r => ({
|
||||
location: [r.lat, r.lon],
|
||||
size: Math.min(0.1, 0.05 + (r.count * 0.005)), // Scale size by visitor count
|
||||
city: r.city,
|
||||
country: r.country
|
||||
})),
|
||||
totalVisitors: total.count,
|
||||
active24h: recent.reduce((sum, r) => sum + r.count, 0)
|
||||
locations,
|
||||
totalVisitors: total?.count || 0,
|
||||
active24h: last24h?.count || 0,
|
||||
active7d: last7d?.count || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Visitor fetch error:', error);
|
||||
return NextResponse.json({ locations: [], totalVisitors: 0, active24h: 0 });
|
||||
return NextResponse.json({
|
||||
locations: [],
|
||||
totalVisitors: 0,
|
||||
active24h: 0,
|
||||
active7d: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user