Add uptime monitor features: fix history bars, lifetime stats, detail modal, custom services
- Fix 24-bar history rendering to always show 24 uniform segments with gray fill for missing hours - Add lifetime uptime % and avg latency to status API - Add clickable detail modal with expanded stats, large history bar, and service removal - Add monitored_services DB table with CRUD API (GET/POST/DELETE) - Monitor reads services from DB each interval, seeds defaults on first run - Add inline form to add custom services to track - Extend log retention from 7 days to 90 days for lifetime stats Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
70
app/api/services/route.ts
Normal file
70
app/api/services/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDb } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const services = await db.all('SELECT id, name, url, created_at FROM monitored_services ORDER BY id ASC');
|
||||
return NextResponse.json(services);
|
||||
} catch (error) {
|
||||
console.error('Services list error:', error);
|
||||
return NextResponse.json([], { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { name, url } = await request.json();
|
||||
|
||||
if (!name || !url) {
|
||||
return NextResponse.json({ error: 'Name and URL are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Basic URL validation
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid URL format' }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
await db.run('INSERT INTO monitored_services (name, url) VALUES (?, ?)', name.trim(), url.trim());
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : 'Unknown error';
|
||||
if (msg.includes('UNIQUE constraint')) {
|
||||
return NextResponse.json({ error: 'A service with that name already exists' }, { status: 409 });
|
||||
}
|
||||
console.error('Service add error:', error);
|
||||
return NextResponse.json({ error: 'Failed to add service' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const { id } = await request.json();
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'Service ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// Get service name before deleting so we can clean up logs
|
||||
const service = await db.get('SELECT name FROM monitored_services WHERE id = ?', id);
|
||||
if (!service) {
|
||||
return NextResponse.json({ error: 'Service not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
await db.run('DELETE FROM monitored_services WHERE id = ?', id);
|
||||
await db.run('DELETE FROM uptime_logs WHERE service_name = ?', service.name);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Service delete error:', error);
|
||||
return NextResponse.json({ error: 'Failed to delete service' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,26 @@ export async function GET() {
|
||||
GROUP BY service_name
|
||||
`);
|
||||
|
||||
// Get lifetime stats (all available data)
|
||||
const statsLifetime = 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
|
||||
GROUP BY service_name
|
||||
`);
|
||||
|
||||
// Get average latency over last 24h (only for 'up' checks)
|
||||
const avgLatencyRows = await db.all(`
|
||||
SELECT service_name,
|
||||
ROUND(AVG(latency)) as avg_latency
|
||||
FROM uptime_logs
|
||||
WHERE timestamp > datetime('now', '-24 hours')
|
||||
AND status = 'up'
|
||||
AND latency > 0
|
||||
GROUP BY service_name
|
||||
`);
|
||||
|
||||
// Get hourly history for last 24h (24 buckets per service)
|
||||
const history = await db.all(`
|
||||
SELECT service_name,
|
||||
@@ -60,11 +80,15 @@ export async function GET() {
|
||||
const results = live.map(l => {
|
||||
const s24 = stats24h.find(s => s.service_name === l.name);
|
||||
const s7d = stats7d.find(s => s.service_name === l.name);
|
||||
const sLife = statsLifetime.find(s => s.service_name === l.name);
|
||||
const avgLat = avgLatencyRows.find(s => s.service_name === l.name);
|
||||
|
||||
return {
|
||||
...l,
|
||||
uptime24h: s24 ? Math.round((s24.up_count / s24.total) * 100) : 100,
|
||||
uptime7d: s7d ? Math.round((s7d.up_count / s7d.total) * 100) : 100,
|
||||
uptimeLifetime: sLife ? Math.round((sLife.up_count / sLife.total) * 100) : 100,
|
||||
avgLatency24h: avgLat ? avgLat.avg_latency : 0,
|
||||
history: historyMap[l.name] || [],
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user