Fixed and changes
This commit is contained in:
@@ -1,53 +1,55 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getDb } from '@/lib/db';
|
||||||
// Configuration - Update these URLs to match your actual services
|
|
||||||
const SERVICES = [
|
|
||||||
{ name: 'Website', url: 'https://www.akkolli.net' },
|
|
||||||
{ name: 'Gitea', url: 'https://code.akkolli.net' },
|
|
||||||
{ name: 'Nextcloud', url: 'http://localhost:6060' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const results = await Promise.all(
|
try {
|
||||||
SERVICES.map(async (service) => {
|
const db = await getDb();
|
||||||
const start = performance.now();
|
|
||||||
try {
|
|
||||||
// Set a short timeout (e.g., 5 seconds)
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
||||||
|
|
||||||
const response = await fetch(service.url, {
|
// Get live status (most recent log per service)
|
||||||
method: 'HEAD',
|
const live = await db.all(`
|
||||||
signal: controller.signal,
|
SELECT service_name as name, url, status, latency
|
||||||
cache: 'no-store',
|
FROM uptime_logs
|
||||||
});
|
WHERE id IN (SELECT MAX(id) FROM uptime_logs GROUP BY service_name)
|
||||||
|
`);
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
// 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
|
||||||
|
WHERE timestamp > datetime('now', '-24 hours')
|
||||||
|
GROUP BY service_name
|
||||||
|
`);
|
||||||
|
|
||||||
const end = performance.now();
|
// Get 7d stats
|
||||||
const latency = Math.round(end - start);
|
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
|
||||||
|
WHERE timestamp > datetime('now', '-7 days')
|
||||||
|
GROUP BY service_name
|
||||||
|
`);
|
||||||
|
|
||||||
return {
|
// Merge data
|
||||||
name: service.name,
|
const results = live.map(l => {
|
||||||
url: service.url,
|
const s24 = stats24h.find(s => s.service_name === l.name);
|
||||||
status: response.ok ? 'up' : 'down',
|
const s7d = stats7d.find(s => s.service_name === l.name);
|
||||||
latency: latency,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
name: service.name,
|
|
||||||
url: service.url,
|
|
||||||
status: 'down',
|
|
||||||
latency: 0,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
error: 'Unreachable'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json(results);
|
return {
|
||||||
|
...l,
|
||||||
|
uptime24h: s24 ? Math.round((s24.up_count / s24.total) * 100) : 100,
|
||||||
|
uptime7d: s7d ? Math.round((s7d.up_count / s7d.total) * 100) : 100,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(results);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Uptime stats error:', error);
|
||||||
|
// Fallback to simple check if DB fails
|
||||||
|
return NextResponse.json([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
app/api/track/route.ts
Normal file
44
app/api/track/route.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getDb } from '@/lib/db';
|
||||||
|
import geoip from 'geoip-lite';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const headers = req.headers;
|
||||||
|
|
||||||
|
// Get IP (proxies can complicate this, but X-Forwarded-For is standard)
|
||||||
|
const forwarded = headers.get('x-forwarded-for');
|
||||||
|
const ip = forwarded ? forwarded.split(',')[0] : '127.0.0.1'; // Fallback for dev
|
||||||
|
|
||||||
|
// Geo lookup
|
||||||
|
const geo = geoip.lookup(ip);
|
||||||
|
|
||||||
|
// Hash IP for privacy (simple hash for demo, salt in prod)
|
||||||
|
// For this personal dash, we might just store it or a simple hash
|
||||||
|
const ipHash = ip; // Storing raw IP for personal dash context, or hash if preferred.
|
||||||
|
|
||||||
|
if (geo) {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.run(
|
||||||
|
`INSERT INTO visitors (ip_hash, city, country, lat, lon) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
ipHash, geo.city, geo.country, geo.ll[0], geo.ll[1]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Fallback for localhost testing
|
||||||
|
if (ip === '127.0.0.1' || ip === '::1') {
|
||||||
|
const db = await getDb();
|
||||||
|
// Mock NYC for localhost
|
||||||
|
await db.run(
|
||||||
|
`INSERT INTO visitors (ip_hash, city, country, lat, lon) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
'localhost', 'New York', 'US', 40.7128, -74.0060
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Track error:', error);
|
||||||
|
return NextResponse.json({ success: false }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/api/visitors/route.ts
Normal file
35
app/api/visitors/route.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getDb } from '@/lib/db';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const db = await getDb();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
const total = await db.get(`SELECT count(*) as count FROM visitors`);
|
||||||
|
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Visitor fetch error:', error);
|
||||||
|
return NextResponse.json({ locations: [], totalVisitors: 0, active24h: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export function GridShell({ children }: GridShellProps) {
|
|||||||
<header className="mb-8 flex items-center justify-between">
|
<header className="mb-8 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-white mb-1">Mission Control</h1>
|
<h1 className="text-3xl font-bold tracking-tight text-white mb-1">Mission Control</h1>
|
||||||
<p className="text-neutral-400 text-sm">System Status & Global Overview</p>
|
<h1 className="text-3xl font-bold tracking-tight text-white mb-1">Mission Control</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{/* Future header controls */}
|
{/* Future header controls */}
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import createGlobe from 'cobe';
|
import createGlobe from 'cobe';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Globe } from 'lucide-react';
|
import { Globe } from 'lucide-react';
|
||||||
|
|
||||||
export function GlobeCard() {
|
export function GlobeCard() {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [visitorStats, setVisitorStats] = useState({ total: 0, active24h: 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,
|
||||||
|
locations: data.locations || []
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load visitors', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchVisitors();
|
||||||
|
const interval = setInterval(fetchVisitors, 30000); // 30s poll
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let phi = 0;
|
let phi = 0;
|
||||||
@@ -25,17 +45,8 @@ export function GlobeCard() {
|
|||||||
baseColor: [0.3, 0.3, 0.3],
|
baseColor: [0.3, 0.3, 0.3],
|
||||||
markerColor: [0.1, 0.8, 1],
|
markerColor: [0.1, 0.8, 1],
|
||||||
glowColor: [0.1, 0.1, 0.2],
|
glowColor: [0.1, 0.1, 0.2],
|
||||||
markers: [
|
markers: visitorStats.locations,
|
||||||
// Mock data points
|
|
||||||
{ location: [40.7128, -74.0060], size: 0.05 }, // NY
|
|
||||||
{ location: [51.5074, -0.1278], size: 0.05 }, // London
|
|
||||||
{ location: [35.6762, 139.6503], size: 0.05 }, // Tokyo
|
|
||||||
{ location: [22.3193, 114.1694], size: 0.05 }, // HK
|
|
||||||
{ location: [-33.8688, 151.2093], size: 0.05 }, // Sydney
|
|
||||||
],
|
|
||||||
onRender: (state) => {
|
onRender: (state) => {
|
||||||
// Called on every animation frame.
|
|
||||||
// `state` will be an empty object, return updated params.
|
|
||||||
state.phi = phi;
|
state.phi = phi;
|
||||||
phi += 0.01;
|
phi += 0.01;
|
||||||
},
|
},
|
||||||
@@ -44,18 +55,27 @@ export function GlobeCard() {
|
|||||||
return () => {
|
return () => {
|
||||||
globe.destroy();
|
globe.destroy();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [visitorStats.locations]);
|
||||||
|
|
||||||
return (
|
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="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="absolute top-6 left-6 z-10 pointer-events-none">
|
||||||
<div className="flex items-center gap-2 text-neutral-400">
|
<div className="flex items-center gap-2 text-neutral-400">
|
||||||
<Globe size={18} />
|
<Globe size={18} />
|
||||||
<span className="text-sm font-medium">Active Visitors</span>
|
<span className="text-sm font-medium">Visitor Map</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1">
|
<div className="mt-4 space-y-2">
|
||||||
<span className="text-2xl font-bold text-white tracking-tight">1,240</span>
|
<div>
|
||||||
<span className="text-xs text-neutral-500 ml-2 font-mono">LIVE</span>
|
<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>
|
||||||
|
</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">TOTAL</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,22 @@ interface ServiceStatus {
|
|||||||
url: string;
|
url: string;
|
||||||
status: 'up' | 'down';
|
status: 'up' | 'down';
|
||||||
latency: number;
|
latency: number;
|
||||||
|
uptime24h?: number;
|
||||||
|
uptime7d?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We'd ideally fetch stats from API, but for now we calculate from live data or mock
|
||||||
|
// To do this properly, we need an API endpoint returning stats.
|
||||||
|
// Let's update `api/status` to also return stats or create `api/status/stats`.
|
||||||
|
// For this step, I'll update the visual to SHOW where stats would be,
|
||||||
|
// and we'll implement the backend stats aggregation in the next step.
|
||||||
|
|
||||||
export function UptimeCard() {
|
export function UptimeCard() {
|
||||||
const [services, setServices] = useState<ServiceStatus[]>([]);
|
const [services, setServices] = useState<ServiceStatus[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/status');
|
const res = await fetch('/api/status');
|
||||||
if (!res.ok) throw new Error('Failed to fetch');
|
if (!res.ok) throw new Error('Failed to fetch');
|
||||||
@@ -33,15 +40,10 @@ export function UptimeCard() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
// Poll every 30 seconds
|
|
||||||
const interval = setInterval(fetchStatus, 30000);
|
const interval = setInterval(fetchStatus, 30000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Calculate overall health
|
|
||||||
const upCount = services.filter(s => s.status === 'up').length;
|
|
||||||
const healthPercentage = services.length > 0 ? Math.round((upCount / services.length) * 100) : 0;
|
|
||||||
|
|
||||||
return (
|
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="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 justify-between items-start">
|
||||||
@@ -50,11 +52,6 @@ export function UptimeCard() {
|
|||||||
<span className="text-sm font-medium">Uptime Monitor</span>
|
<span className="text-sm font-medium">Uptime Monitor</span>
|
||||||
{loading && <RefreshCcw size={12} className="animate-spin ml-2 opacity-50" />}
|
{loading && <RefreshCcw size={12} className="animate-spin ml-2 opacity-50" />}
|
||||||
</div>
|
</div>
|
||||||
{!loading && !error && (
|
|
||||||
<span className={`text-xs px-2 py-1 rounded-full font-mono ${healthPercentage === 100 ? 'bg-emerald-500/10 text-emerald-500' : 'bg-red-500/10 text-red-500'}`}>
|
|
||||||
{healthPercentage}%
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
@@ -63,26 +60,30 @@ export function UptimeCard() {
|
|||||||
<span className="text-xs">Failed to load status</span>
|
<span className="text-xs">Failed to load status</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3 mt-4">
|
<div className="space-y-4 mt-4">
|
||||||
{services.length === 0 && loading ? (
|
{services.map((service) => (
|
||||||
// Skeleton loader
|
<div key={service.name} className="flex flex-col gap-1">
|
||||||
[1, 2, 3].map(i => (
|
<div className="flex items-center justify-between group">
|
||||||
<div key={i} className="flex items-center justify-between animate-pulse">
|
|
||||||
<div className="h-4 w-20 bg-neutral-800 rounded"></div>
|
|
||||||
<div className="h-2 w-2 bg-neutral-800 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
services.map((service) => (
|
|
||||||
<div key={service.name} 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>
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-neutral-500 font-mono">{service.status === 'up' ? `${service.latency}ms` : 'DOWN'}</span>
|
<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'}`}>
|
||||||
<div className={`w-2 h-2 rounded-full shadow-[0_0_8px_rgba(0,0,0,0.4)] ${service.status === 'up' ? 'bg-emerald-500 shadow-emerald-500/40' : 'bg-red-500 shadow-red-500/40'}`} />
|
{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>
|
</div>
|
||||||
))
|
{/* Mini bars visualization for history - Mocked visual for now until API is ready */}
|
||||||
)}
|
<div className="flex gap-[2px] h-1.5 opacity-50">
|
||||||
|
{[...Array(20)].map((_, i) => (
|
||||||
|
<div key={i} className={`flex-1 rounded-full ${Math.random() > 0.95 ? 'bg-red-500' : 'bg-emerald-500'}`} />
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function WeatherCard() {
|
|||||||
async function fetchWeather() {
|
async function fetchWeather() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LNG}¤t=temperature_2m,relative_humidity_2m,weather_code&daily=temperature_2m_max,temperature_2m_min&temperature_unit=fahrenheit&timezone=auto`
|
`https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LNG}¤t=temperature_2m,relative_humidity_2m,weather_code&daily=temperature_2m_max,temperature_2m_min&temperature_unit=celsius&timezone=auto`
|
||||||
);
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setWeather(data);
|
setWeather(data);
|
||||||
@@ -62,7 +62,7 @@ export function WeatherCard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-end gap-4 mt-2">
|
<div className="flex items-end gap-4 mt-2">
|
||||||
<div className="text-4xl font-bold text-white tracking-tighter">{Math.round(current.temperature_2m)}°F</div>
|
<div className="text-4xl font-bold text-white tracking-tighter">{Math.round(current.temperature_2m)}°C</div>
|
||||||
<div className="pb-1 text-sm text-neutral-400 font-medium">{getWeatherDescription(current.weather_code)}</div>
|
<div className="pb-1 text-sm text-neutral-400 font-medium">{getWeatherDescription(current.weather_code)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
39
lib/db.ts
Normal file
39
lib/db.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import sqlite3 from 'sqlite3';
|
||||||
|
import { open, Database } from 'sqlite';
|
||||||
|
|
||||||
|
let db: Database | null = null;
|
||||||
|
|
||||||
|
export async function getDb() {
|
||||||
|
if (db) return db;
|
||||||
|
|
||||||
|
// Enable verbose mode for debugging
|
||||||
|
sqlite3.verbose();
|
||||||
|
|
||||||
|
db = await open({
|
||||||
|
filename: './dashboard.db',
|
||||||
|
driver: sqlite3.Database
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS uptime_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
service_name TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
latency INTEGER,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS visitors (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ip_hash TEXT,
|
||||||
|
city TEXT,
|
||||||
|
country TEXT,
|
||||||
|
lat REAL,
|
||||||
|
lon REAL,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
78
monitor.js
Normal file
78
monitor.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
const sqlite3 = require('sqlite3');
|
||||||
|
const { open } = require('sqlite');
|
||||||
|
const fetch = require('node-fetch'); // Native fetch in Node 18, but let's be safe or use global fetch
|
||||||
|
// Node 18 has global fetch, so we don't need node-fetch if running with node 18
|
||||||
|
|
||||||
|
const SERVICES = [
|
||||||
|
{ name: 'Website', url: 'https://www.akkolli.net' },
|
||||||
|
{ name: 'Gitea', url: 'https://code.akkolli.net' },
|
||||||
|
{ name: 'Nextcloud', url: 'http://localhost:6060' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function monitor() {
|
||||||
|
console.log('Starting monitoring loop...');
|
||||||
|
|
||||||
|
const db = await open({
|
||||||
|
filename: './dashboard.db',
|
||||||
|
driver: sqlite3.Database
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure table exists (in case monitor runs before app)
|
||||||
|
await db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS uptime_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
service_name TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
latency INTEGER,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
console.log('Running checks...');
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
for (const service of SERVICES) {
|
||||||
|
const start = performance.now();
|
||||||
|
let status = 'down';
|
||||||
|
let latency = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
|
const res = await fetch(service.url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
status = res.ok ? 'up' : 'down';
|
||||||
|
const end = performance.now();
|
||||||
|
latency = Math.round(end - start);
|
||||||
|
} catch (err) {
|
||||||
|
status = 'down';
|
||||||
|
latency = 0;
|
||||||
|
// console.error(`Failed to reach ${service.name}:`, err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.run(
|
||||||
|
`INSERT INTO uptime_logs (service_name, url, status, latency, timestamp) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
service.name, service.url, status, latency, now
|
||||||
|
);
|
||||||
|
} catch (dbErr) {
|
||||||
|
console.error('DB Write Error:', dbErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune old logs (keep 7 days)
|
||||||
|
try {
|
||||||
|
await db.run(`DELETE FROM uptime_logs WHERE timestamp < datetime('now', '-7 days')`);
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
|
}, 60000); // Run every minute
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor();
|
||||||
23
monitor.log
Normal file
23
monitor.log
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
nohup: ignoring input
|
||||||
|
node:internal/modules/cjs/loader:1137
|
||||||
|
throw err;
|
||||||
|
^
|
||||||
|
|
||||||
|
Error: Cannot find module 'sqlite'
|
||||||
|
Require stack:
|
||||||
|
- /home/akkolli/projects/webserver/admin_dash/monitor.js
|
||||||
|
at Module._resolveFilename (node:internal/modules/cjs/loader:1134:15)
|
||||||
|
at Module._load (node:internal/modules/cjs/loader:975:27)
|
||||||
|
at Module.require (node:internal/modules/cjs/loader:1225:19)
|
||||||
|
at require (node:internal/modules/helpers:177:18)
|
||||||
|
at Object.<anonymous> (/home/akkolli/projects/webserver/admin_dash/monitor.js:2:18)
|
||||||
|
at Module._compile (node:internal/modules/cjs/loader:1356:14)
|
||||||
|
at Module._extensions..js (node:internal/modules/cjs/loader:1414:10)
|
||||||
|
at Module.load (node:internal/modules/cjs/loader:1197:32)
|
||||||
|
at Module._load (node:internal/modules/cjs/loader:1013:12)
|
||||||
|
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:128:12) {
|
||||||
|
code: 'MODULE_NOT_FOUND',
|
||||||
|
requireStack: [ '/home/akkolli/projects/webserver/admin_dash/monitor.js' ]
|
||||||
|
}
|
||||||
|
|
||||||
|
Node.js v18.19.1
|
||||||
@@ -1,6 +1,23 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
externals: {
|
||||||
|
'geoip-lite': 'commonjs geoip-lite',
|
||||||
|
'sqlite3': 'commonjs sqlite3',
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
// This is needed to copy the geoip-lite data files to the standalone build
|
||||||
|
outputFileTracingIncludes: {
|
||||||
|
'/api/track': ['./node_modules/geoip-lite/data/**/*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
webpack: (config) => {
|
||||||
|
config.externals.push({
|
||||||
|
'geoip-lite': 'commonjs geoip-lite',
|
||||||
|
'sqlite3': 'commonjs sqlite3',
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
1630
package-lock.json
generated
1630
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,19 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/geoip-lite": "^1.4.4",
|
||||||
|
"@types/sqlite3": "^5.1.0",
|
||||||
"cobe": "^0.6.5",
|
"cobe": "^0.6.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"geoip-lite": "^1.4.10",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "14.2.15",
|
"next": "14.2.15",
|
||||||
|
"open": "^11.0.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"rss-parser": "^3.13.0"
|
"rss-parser": "^3.13.0",
|
||||||
|
"sqlite": "^5.1.1",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
Reference in New Issue
Block a user