Files
location-mqtt-tracker-app/app/admin/page.tsx
Joachim Hummel bae6728f3f Add Geofence frontend UI with management and event history
Implemented complete frontend for the Geofence MVP feature:

**Pages:**
- /admin/geofences - Management page with create/edit/delete modals
- /admin/geofences/events - Event history with stats and filters
- Dashboard widget showing active geofences and recent events

**Features:**
- Create/Edit geofences with device selection, coordinates, radius, and color
- Toggle active/inactive status
- View enter/exit events with notification status
- Auto-refresh every 30 seconds
- Zone limit enforcement (5 for users, unlimited for admins)
- Stats cards showing total events, enters, exits, and notifications

**API:**
- GET /api/geofences/events - Fetch events with optional filters

All frontend components follow the existing admin panel design system
with gradient backgrounds, shadow effects, and responsive layouts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 23:07:29 +00:00

726 lines
31 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { LocationResponse } from "@/lib/types";
interface DeviceInfo {
id: string;
name: string;
color: string;
}
export default function AdminDashboard() {
const { data: session } = useSession();
const userRole = (session?.user as any)?.role;
const username = session?.user?.name || '';
const isAdmin = userRole === 'ADMIN';
const isSuperAdmin = username === 'admin';
const [stats, setStats] = useState({
totalDevices: 0,
totalPoints: 0,
lastUpdated: "",
onlineDevices: 0,
});
const [devices, setDevices] = useState<DeviceInfo[]>([]);
const [cleanupStatus, setCleanupStatus] = useState<{
loading: boolean;
message: string;
type: 'success' | 'error' | '';
}>({
loading: false,
message: '',
type: '',
});
const [optimizeStatus, setOptimizeStatus] = useState<{
loading: boolean;
message: string;
type: 'success' | 'error' | '';
}>({
loading: false,
message: '',
type: '',
});
const [dbStats, setDbStats] = useState<any>(null);
const [systemStatus, setSystemStatus] = useState<any>(null);
const [geofenceStats, setGeofenceStats] = useState<any>(null);
// Fetch devices from API
useEffect(() => {
const fetchDevices = async () => {
try {
const response = await fetch("/api/devices/public");
if (response.ok) {
const data = await response.json();
setDevices(data.devices);
}
} catch (err) {
console.error("Failed to fetch devices:", err);
}
};
fetchDevices();
// Refresh devices every 30 seconds
const interval = setInterval(fetchDevices, 30000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const fetchStats = async () => {
try {
// Fetch from local API (reads from SQLite cache)
const response = await fetch("/api/locations?sync=false"); // sync=false for faster response
const data: LocationResponse = await response.json();
// Calculate online devices (last location < 10 minutes)
const now = Date.now();
const tenMinutesAgo = now - 10 * 60 * 1000;
const recentDevices = new Set(
data.history
.filter((loc) => {
const locationTime = new Date(loc.timestamp).getTime();
return locationTime > tenMinutesAgo;
})
.map((loc) => loc.username)
);
setStats({
totalDevices: devices.length,
totalPoints: data.total_points || data.history.length,
lastUpdated: data.last_updated || new Date().toISOString(),
onlineDevices: recentDevices.size,
});
} catch (err) {
console.error("Failed to fetch stats", err);
}
};
if (devices.length > 0) {
fetchStats();
const interval = setInterval(fetchStats, 10000);
return () => clearInterval(interval);
}
}, [devices]);
// Fetch detailed database statistics
useEffect(() => {
const fetchDbStats = async () => {
try {
const response = await fetch('/api/locations/stats');
if (response.ok) {
const data = await response.json();
setDbStats(data);
}
} catch (err) {
console.error('Failed to fetch DB stats:', err);
}
};
fetchDbStats();
// Refresh DB stats every 30 seconds
const interval = setInterval(fetchDbStats, 30000);
return () => clearInterval(interval);
}, []);
// Fetch system status (uptime, memory)
useEffect(() => {
const fetchSystemStatus = async () => {
try {
const response = await fetch('/api/system/status');
if (response.ok) {
const data = await response.json();
setSystemStatus(data);
}
} catch (err) {
console.error('Failed to fetch system status:', err);
}
};
fetchSystemStatus();
// Refresh every 10 seconds for live uptime
const interval = setInterval(fetchSystemStatus, 10000);
return () => clearInterval(interval);
}, []);
// Fetch geofence statistics
useEffect(() => {
const fetchGeofenceStats = async () => {
try {
const [geofencesRes, eventsRes] = await Promise.all([
fetch('/api/geofences'),
fetch('/api/geofences/events?limit=100'),
]);
if (geofencesRes.ok && eventsRes.ok) {
const geofencesData = await geofencesRes.json();
const eventsData = await eventsRes.json();
const now = Date.now();
const oneDayAgo = now - 24 * 60 * 60 * 1000;
const events24h = eventsData.events?.filter((e: any) =>
new Date(e.timestamp).getTime() > oneDayAgo
) || [];
setGeofenceStats({
total: geofencesData.total || 0,
active: geofencesData.geofences?.filter((g: any) => g.is_active === 1).length || 0,
events24h: events24h.length,
recentEvents: eventsData.events?.slice(0, 5) || [],
});
}
} catch (err) {
console.error('Failed to fetch geofence stats:', err);
}
};
fetchGeofenceStats();
// Refresh every 30 seconds
const interval = setInterval(fetchGeofenceStats, 30000);
return () => clearInterval(interval);
}, []);
// Cleanup old locations
const handleCleanup = async (retentionHours: number) => {
if (!confirm(`Delete all locations older than ${Math.round(retentionHours / 24)} days?`)) {
return;
}
setCleanupStatus({ loading: true, message: '', type: '' });
try {
const response = await fetch('/api/locations/cleanup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ retentionHours }),
});
const data = await response.json();
if (response.ok) {
setCleanupStatus({
loading: false,
message: `✓ Deleted ${data.deleted} records. Freed ${data.freedKB} KB.`,
type: 'success',
});
// Refresh stats
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
setCleanupStatus({
loading: false,
message: `Error: ${data.error}`,
type: 'error',
});
}
} catch (error) {
setCleanupStatus({
loading: false,
message: 'Failed to cleanup locations',
type: 'error',
});
}
// Clear message after 5 seconds
setTimeout(() => {
setCleanupStatus({ loading: false, message: '', type: '' });
}, 5000);
};
// Optimize database
const handleOptimize = async () => {
if (!confirm('Optimize database? This may take a few seconds.')) {
return;
}
setOptimizeStatus({ loading: true, message: '', type: '' });
try {
const response = await fetch('/api/locations/optimize', {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
setOptimizeStatus({
loading: false,
message: `✓ Database optimized. Freed ${data.freedMB} MB. (${data.before.sizeMB}${data.after.sizeMB} MB)`,
type: 'success',
});
// Refresh stats
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
setOptimizeStatus({
loading: false,
message: `Error: ${data.error}`,
type: 'error',
});
}
} catch (error) {
setOptimizeStatus({
loading: false,
message: 'Failed to optimize database',
type: 'error',
});
}
// Clear message after 5 seconds
setTimeout(() => {
setOptimizeStatus({ loading: false, message: '', type: '' });
}, 5000);
};
const statCards = [
{
title: "Total Devices",
value: stats.totalDevices,
icon: "📱",
},
{
title: "Online Devices",
value: stats.onlineDevices,
icon: "🟢",
},
{
title: "Total Locations",
value: stats.totalPoints,
icon: "📍",
},
];
return (
<div className="space-y-8">
{/* Hero Section with Gradient */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800 p-8 shadow-xl">
<div className="absolute top-0 right-0 -mt-4 -mr-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-40 w-40 rounded-full bg-white/10 blur-3xl"></div>
<div className="relative">
<h2 className="text-4xl font-bold text-white mb-2">Dashboard</h2>
<p className="text-blue-100 text-lg">Willkommen zurück! Hier ist ein Überblick über dein System.</p>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{statCards.map((stat, index) => {
const gradients = [
'from-emerald-500 to-teal-600',
'from-blue-500 to-indigo-600',
'from-violet-500 to-purple-600'
];
return (
<div
key={stat.title}
className="group relative overflow-hidden bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1"
>
<div className={`absolute inset-0 bg-gradient-to-br ${gradients[index]} opacity-0 group-hover:opacity-5 transition-opacity`}></div>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className={`flex items-center justify-center w-14 h-14 rounded-xl bg-gradient-to-br ${gradients[index]} text-white text-2xl shadow-lg`}>
{stat.icon}
</div>
</div>
<p className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-1">{stat.title}</p>
<p className="text-4xl font-bold bg-gradient-to-br from-gray-900 to-gray-700 bg-clip-text text-transparent">{stat.value}</p>
</div>
</div>
);
})}
</div>
{/* System Status */}
{systemStatus && (
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="bg-gradient-to-r from-slate-50 to-gray-50 px-6 py-5 border-b border-gray-200">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-slate-600 to-slate-700 flex items-center justify-center text-white text-xl">
</div>
<h3 className="text-xl font-bold text-gray-900">
System Status
</h3>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="relative overflow-hidden bg-gradient-to-br from-green-50 to-emerald-50 p-5 rounded-xl border border-green-100">
<div className="absolute top-0 right-0 text-6xl opacity-10"></div>
<p className="text-sm font-semibold text-green-700 uppercase tracking-wide mb-1">App Uptime</p>
<p className="text-3xl font-bold text-green-900">{systemStatus.uptime.formatted}</p>
<p className="text-xs text-green-600 mt-2">Running since server start</p>
</div>
<div className="relative overflow-hidden bg-gradient-to-br from-blue-50 to-indigo-50 p-5 rounded-xl border border-blue-100">
<div className="absolute top-0 right-0 text-6xl opacity-10">💾</div>
<p className="text-sm font-semibold text-blue-700 uppercase tracking-wide mb-1">Memory Usage</p>
<p className="text-3xl font-bold text-blue-900">{systemStatus.memory.heapUsed} MB</p>
<p className="text-xs text-blue-600 mt-2">Heap: {systemStatus.memory.heapTotal} MB / RSS: {systemStatus.memory.rss} MB</p>
</div>
<div className="relative overflow-hidden bg-gradient-to-br from-purple-50 to-violet-50 p-5 rounded-xl border border-purple-100">
<div className="absolute top-0 right-0 text-6xl opacity-10">🚀</div>
<p className="text-sm font-semibold text-purple-700 uppercase tracking-wide mb-1">Runtime</p>
<p className="text-3xl font-bold text-purple-900">{systemStatus.nodejs}</p>
<p className="text-xs text-purple-600 mt-2">Platform: {systemStatus.platform}</p>
</div>
</div>
</div>
</div>
)}
{/* Database Statistics */}
{dbStats && (
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="bg-gradient-to-r from-amber-50 to-orange-50 px-6 py-5 border-b border-orange-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-600 to-orange-600 flex items-center justify-center text-white text-xl">
📊
</div>
<h3 className="text-xl font-bold text-gray-900">
Database Statistics
</h3>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="relative overflow-hidden bg-gradient-to-br from-cyan-50 to-blue-50 p-5 rounded-xl border border-cyan-100">
<div className="absolute top-0 right-0 text-6xl opacity-10">💽</div>
<p className="text-sm font-semibold text-cyan-700 uppercase tracking-wide mb-1">Database Size</p>
<p className="text-3xl font-bold text-cyan-900">{dbStats.sizeMB} MB</p>
<p className="text-xs text-cyan-600 mt-2">WAL Mode: {dbStats.walMode}</p>
</div>
<div className="relative overflow-hidden bg-gradient-to-br from-indigo-50 to-blue-50 p-5 rounded-xl border border-indigo-100">
<div className="absolute top-0 right-0 text-6xl opacity-10">📅</div>
<p className="text-sm font-semibold text-indigo-700 uppercase tracking-wide mb-1">Time Range</p>
<p className="text-sm font-semibold text-indigo-900">
{dbStats.oldest ? new Date(dbStats.oldest).toLocaleDateString() : 'N/A'}
</p>
<p className="text-xs text-indigo-600">to</p>
<p className="text-sm font-semibold text-indigo-900">
{dbStats.newest ? new Date(dbStats.newest).toLocaleDateString() : 'N/A'}
</p>
</div>
<div className="relative overflow-hidden bg-gradient-to-br from-pink-50 to-rose-50 p-5 rounded-xl border border-pink-100">
<div className="absolute top-0 right-0 text-6xl opacity-10">📈</div>
<p className="text-sm font-semibold text-pink-700 uppercase tracking-wide mb-1">Average Per Day</p>
<p className="text-3xl font-bold text-pink-900">{dbStats.avgPerDay}</p>
<p className="text-xs text-pink-600 mt-2">locations (last 7 days)</p>
</div>
</div>
{/* Locations per Device */}
{dbStats.perDevice && dbStats.perDevice.length > 0 && (
<div className="bg-gradient-to-br from-gray-50 to-slate-50 p-5 rounded-xl border border-gray-200">
<h4 className="text-sm font-bold text-gray-700 uppercase tracking-wide mb-4 flex items-center gap-2">
<span className="text-lg">📱</span>
Locations per Device
</h4>
<div className="space-y-2">
{dbStats.perDevice.map((device: any, idx: number) => (
<div key={device.username} className="group flex items-center justify-between py-3 px-4 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow border border-gray-100">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white text-xs font-bold">
{idx + 1}
</div>
<span className="text-sm font-semibold text-gray-700">Device {device.username}</span>
</div>
<span className="text-sm font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent">{device.count.toLocaleString()} locations</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
{/* Geofence Statistics */}
{geofenceStats && (geofenceStats.total > 0 || geofenceStats.events24h > 0) && (
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="bg-gradient-to-r from-purple-50 to-violet-50 px-6 py-5 border-b border-purple-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-purple-600 to-violet-600 flex items-center justify-center text-white text-xl">
📍
</div>
<h3 className="text-xl font-bold text-gray-900">
Geofence Overview
</h3>
</div>
<a
href="/admin/geofences"
className="px-4 py-2 bg-gradient-to-r from-purple-600 to-violet-600 text-white rounded-lg hover:from-purple-700 hover:to-violet-700 font-semibold text-sm shadow-md transition-all"
>
Manage Geofences
</a>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="relative overflow-hidden bg-gradient-to-br from-purple-50 to-violet-50 p-5 rounded-xl border border-purple-100">
<div className="absolute top-0 right-0 text-6xl opacity-10">🗺</div>
<p className="text-sm font-semibold text-purple-700 uppercase tracking-wide mb-1">Active Geofences</p>
<p className="text-3xl font-bold text-purple-900">{geofenceStats.active}</p>
<p className="text-xs text-purple-600 mt-2">of {geofenceStats.total} total zones</p>
</div>
<div className="relative overflow-hidden bg-gradient-to-br from-blue-50 to-indigo-50 p-5 rounded-xl border border-blue-100">
<div className="absolute top-0 right-0 text-6xl opacity-10">📊</div>
<p className="text-sm font-semibold text-blue-700 uppercase tracking-wide mb-1">Events (24h)</p>
<p className="text-3xl font-bold text-blue-900">{geofenceStats.events24h}</p>
<p className="text-xs text-blue-600 mt-2">enter/exit events today</p>
</div>
<div className="relative overflow-hidden bg-gradient-to-br from-green-50 to-emerald-50 p-5 rounded-xl border border-green-100">
<div className="absolute top-0 right-0 text-6xl opacity-10"></div>
<p className="text-sm font-semibold text-green-700 uppercase tracking-wide mb-1">Status</p>
<p className="text-3xl font-bold text-green-900">
{geofenceStats.active > 0 ? '✓ Active' : 'Inactive'}
</p>
<p className="text-xs text-green-600 mt-2">monitoring enabled</p>
</div>
</div>
{/* Recent Events */}
{geofenceStats.recentEvents && geofenceStats.recentEvents.length > 0 && (
<div className="bg-gradient-to-br from-gray-50 to-slate-50 p-5 rounded-xl border border-gray-200">
<div className="flex items-center justify-between mb-4">
<h4 className="text-sm font-bold text-gray-700 uppercase tracking-wide flex items-center gap-2">
<span className="text-lg">📋</span>
Recent Events
</h4>
<a
href="/admin/geofences/events"
className="text-sm text-purple-600 hover:text-purple-700 font-semibold"
>
View All
</a>
</div>
<div className="space-y-2">
{geofenceStats.recentEvents.map((event: any, idx: number) => (
<div key={event.id || idx} className="flex items-center justify-between py-3 px-4 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow border border-gray-100">
<div className="flex items-center gap-3">
<div
className="w-3 h-3 rounded-full ring-2 ring-white shadow-sm"
style={{ backgroundColor: event.geofenceColor || '#gray' }}
/>
<div>
<span className="text-sm font-semibold text-gray-900">
{event.geofenceName || 'Unknown Zone'}
</span>
<p className="text-xs text-gray-500">
Device {event.device_id} {' '}
{new Date(event.timestamp).toLocaleString()}
</p>
</div>
</div>
{event.event_type === 'enter' ? (
<span className="px-2 py-1 rounded-full text-xs font-bold bg-green-100 text-green-800">
Enter
</span>
) : (
<span className="px-2 py-1 rounded-full text-xs font-bold bg-orange-100 text-orange-800">
Exit
</span>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
{/* Device List */}
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="bg-gradient-to-r from-teal-50 to-cyan-50 px-6 py-5 border-b border-teal-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-teal-600 to-cyan-600 flex items-center justify-center text-white text-xl">
📱
</div>
<h3 className="text-xl font-bold text-gray-900">
Configured Devices
</h3>
</div>
</div>
<div className="p-6">
<div className="overflow-x-auto">
<table className="min-w-full">
<thead>
<tr className="border-b-2 border-gray-200">
<th className="text-left py-4 px-4 text-xs font-bold text-gray-600 uppercase tracking-wider">
ID
</th>
<th className="text-left py-4 px-4 text-xs font-bold text-gray-600 uppercase tracking-wider">
Name
</th>
<th className="text-left py-4 px-4 text-xs font-bold text-gray-600 uppercase tracking-wider">
Color
</th>
</tr>
</thead>
<tbody>
{devices.map((device, idx) => (
<tr
key={device.id}
className="border-b border-gray-100 hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 transition-all"
>
<td className="py-4 px-4 text-sm font-semibold text-gray-900">
{device.id}
</td>
<td className="py-4 px-4 text-sm font-semibold text-gray-900">
{device.name}
</td>
<td className="py-4 px-4">
<div className="flex items-center gap-3">
<div
className="w-8 h-8 rounded-lg shadow-md ring-2 ring-white"
style={{ backgroundColor: device.color }}
/>
<span className="text-sm font-mono text-gray-600 bg-gray-100 px-2 py-1 rounded">
{device.color}
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Database Maintenance - SUPER ADMIN ONLY (username "admin") */}
{isSuperAdmin && (
<div className="bg-white rounded-2xl shadow-lg overflow-hidden border border-red-100">
<div className="bg-gradient-to-r from-red-50 to-orange-50 px-6 py-5 border-b border-red-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-red-600 to-orange-600 flex items-center justify-center text-white text-xl">
🛠
</div>
<div>
<h3 className="text-xl font-bold text-gray-900">
Database Maintenance
</h3>
<p className="text-xs text-red-600 font-semibold">SUPER ADMIN ONLY</p>
</div>
</div>
</div>
<div className="p-6 space-y-6">
{/* Cleanup Section */}
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-xl border border-blue-100">
<h4 className="text-base font-bold text-gray-900 mb-2 flex items-center gap-2">
<span className="text-xl">🧹</span>
Clean up old data
</h4>
<p className="text-sm text-gray-700 mb-4">
Delete old location data to keep the database size manageable.
</p>
{/* Cleanup Status Message */}
{cleanupStatus.message && (
<div
className={`mb-4 p-4 rounded-lg font-semibold ${
cleanupStatus.type === 'success'
? 'bg-green-100 text-green-800 border border-green-200'
: 'bg-red-100 text-red-800 border border-red-200'
}`}
>
{cleanupStatus.message}
</div>
)}
{/* Cleanup Buttons */}
<div className="flex flex-wrap gap-3">
<button
onClick={() => handleCleanup(168)}
disabled={cleanupStatus.loading}
className="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed font-semibold shadow-md hover:shadow-lg transition-all"
>
{cleanupStatus.loading ? 'Cleaning...' : 'Delete > 7 days'}
</button>
<button
onClick={() => handleCleanup(360)}
disabled={cleanupStatus.loading}
className="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed font-semibold shadow-md hover:shadow-lg transition-all"
>
{cleanupStatus.loading ? 'Cleaning...' : 'Delete > 15 days'}
</button>
<button
onClick={() => handleCleanup(720)}
disabled={cleanupStatus.loading}
className="px-5 py-2.5 bg-gradient-to-r from-indigo-600 to-indigo-700 text-white rounded-lg hover:from-indigo-700 hover:to-indigo-800 disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed font-semibold shadow-md hover:shadow-lg transition-all"
>
{cleanupStatus.loading ? 'Cleaning...' : 'Delete > 30 days'}
</button>
<button
onClick={() => handleCleanup(2160)}
disabled={cleanupStatus.loading}
className="px-5 py-2.5 bg-gradient-to-r from-orange-600 to-red-600 text-white rounded-lg hover:from-orange-700 hover:to-red-700 disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed font-semibold shadow-md hover:shadow-lg transition-all"
>
{cleanupStatus.loading ? 'Cleaning...' : 'Delete > 90 days'}
</button>
</div>
<div className="mt-4 bg-white/70 p-3 rounded-lg border border-blue-200">
<p className="text-xs text-gray-600">
<span className="font-semibold">Current database size:</span> {stats.totalPoints.toLocaleString()} locations
</p>
</div>
</div>
{/* Optimize Section */}
<div className="bg-gradient-to-br from-purple-50 to-violet-50 p-6 rounded-xl border border-purple-100">
<h4 className="text-base font-bold text-gray-900 mb-2 flex items-center gap-2">
<span className="text-xl"></span>
Optimize Database
</h4>
<p className="text-sm text-gray-700 mb-4">
Run VACUUM and ANALYZE to reclaim disk space and improve query performance. Recommended after cleanup.
</p>
{/* Optimize Status Message */}
{optimizeStatus.message && (
<div
className={`mb-4 p-4 rounded-lg font-semibold ${
optimizeStatus.type === 'success'
? 'bg-green-100 text-green-800 border border-green-200'
: 'bg-red-100 text-red-800 border border-red-200'
}`}
>
{optimizeStatus.message}
</div>
)}
<button
onClick={handleOptimize}
disabled={optimizeStatus.loading}
className="px-6 py-3 bg-gradient-to-r from-purple-600 to-violet-600 text-white rounded-lg hover:from-purple-700 hover:to-violet-700 disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed font-semibold shadow-md hover:shadow-lg transition-all flex items-center gap-2"
>
<span className="text-xl">{optimizeStatus.loading ? '⚙️' : '⚡'}</span>
{optimizeStatus.loading ? 'Optimizing...' : 'Optimize Now'}
</button>
</div>
</div>
</div>
)}
{/* Last Updated */}
<div className="bg-gradient-to-r from-gray-50 to-slate-50 rounded-xl p-4 border border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-gray-600">
<span className="text-lg">🕒</span>
<span className="text-sm font-semibold">Last Updated</span>
</div>
<span className="text-sm font-mono text-gray-700 bg-white px-3 py-1 rounded-lg shadow-sm">
{new Date(stats.lastUpdated).toLocaleString()}
</span>
</div>
</div>
</div>
);
}