import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom'; import axios from 'axios'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import './App.css'; import SettingsWizard from './SettingsWizard'; import History from './History'; import SLA from './SLA'; import { formatLocalTime, formatChartTime } from './utils/timeUtils'; // Simple duration formatter for incident spans function formatDuration(ms) { if (!ms || ms < 0) return '0m'; const minutes = Math.floor(ms / 60000); const hours = Math.floor(minutes / 60); const mins = minutes % 60; if (hours > 0) return `${hours}h ${mins}m`; return `${mins}m`; } function Dashboard() { const [resources, setResources] = useState([]); const [groups, setGroups] = useState([]); const [showModal, setShowModal] = useState(false); const [showGroupModal, setShowGroupModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [collapsedGroups, setCollapsedGroups] = useState({}); const [editingId, setEditingId] = useState(null); const [editData, setEditData] = useState({}); const [formData, setFormData] = useState({ name: '', url: '', type: 'http', check_interval: 60000, timeout: 5000, group_id: null, http_keyword: '', http_headers: '', quiet_hours_start: '', quiet_hours_end: '', cert_expiry_days: 30, sla_target: 99.9, email_to: '', maintenance_mode: false, }); const [groupData, setGroupData] = useState({ name: '', description: '' }); const navigate = useNavigate(); useEffect(() => { loadResources(); const interval = setInterval(loadResources, 5000); return () => clearInterval(interval); }, []); const loadResources = async () => { try { const response = await axios.get('/api/dashboard'); setResources(response.data.resources); setGroups(response.data.groups || []); } catch (error) { console.error('Error loading resources:', error); } }; const handleCreateGroup = async (e) => { e.preventDefault(); try { await axios.post('/api/groups', groupData); setShowGroupModal(false); setGroupData({ name: '', description: '' }); loadResources(); } catch (error) { console.error('Error creating group:', error); alert('Error creating group'); } }; const handleSubmit = async (e) => { e.preventDefault(); try { await axios.post('/api/resources', formData); setShowModal(false); setFormData({ name: '', url: '', type: 'http', check_interval: 60000, timeout: 5000, group_id: null, http_keyword: '', http_headers: '', quiet_hours_start: '', quiet_hours_end: '', cert_expiry_days: 30, sla_target: 99.9, email_to: '', maintenance_mode: false, }); loadResources(); } catch (error) { console.error('Error creating resource:', error); alert('Error creating resource'); } }; const handleEditResource = async (id) => { try { await axios.put(`/api/resources/${id}`, editData); setShowEditModal(false); setEditData({}); loadResources(); } catch (error) { console.error('Error updating resource:', error); alert('Error updating resource'); } }; const openEditModal = (resource) => { setEditData(resource); setShowEditModal(true); }; const handleDeleteResource = async (id) => { if (window.confirm('Delete this resource?')) { try { await axios.delete(`/api/resources/${id}`); loadResources(); } catch (error) { console.error('Error deleting resource:', error); alert('Error deleting resource'); } } }; const toggleGroup = (groupId) => { setCollapsedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] })); }; const groupResourcesMap = resources.reduce((acc, r) => { const gid = r.group_id || 'ungrouped'; if (!acc[gid]) acc[gid] = []; acc[gid].push(r); return acc; }, {}); const getGroupStats = (groupResources) => { if (!groupResources || groupResources.length === 0) return { uptime: 0, avgResponse: 0, total: 0 }; const uptime = (groupResources.reduce((sum, r) => sum + parseFloat(r.uptime || 0), 0) / groupResources.length).toFixed(2); const avgResponse = (groupResources.reduce((sum, r) => sum + parseInt(r.avgResponseTime || 0), 0) / groupResources.length).toFixed(0); return { uptime, avgResponse, total: groupResources.length }; }; const renderResourceCard = (resource) => { const sparkData = (resource.recentChecks || []).map((c, idx) => ({ idx, responseTime: c.response_time || 0, statusValue: c.status === 'up' ? 1 : 0, })); const isEditing = editingId === resource.id; return (
!isEditing && !e.target.closest('.card-actions') && navigate(`/resource/${resource.id}`)} >
{isEditing ? ( setEditData({ ...editData, name: e.target.value })} onClick={(e) => e.stopPropagation()} className="edit-input" placeholder="Resource name" /> ) : (

{resource.name}

)}

{resource.url}

Type: {resource.type}

{resource.status}
{resource.hasActiveIncident && (
⚠️ Active Incident
)}

{ e.stopPropagation(); if (!isEditing) setEditingId(resource.id); }}> {resource.uptime}%

Uptime (24h)

{ e.stopPropagation(); if (!isEditing) setEditingId(resource.id); }}> {resource.avgResponseTime}ms

Avg Resp

{sparkData.length > 1 ? ( ) : ( No recent data )}
{!isEditing && resource.lastCheck && (

Last: {formatLocalTime(resource.lastCheck)}

)}
); }; return (

SkyWatch Dashboard

{resources.length === 0 ? (

No resources yet

Add your first resource to start monitoring

) : (
{groups.map((group) => { const groupStats = getGroupStats(groupResourcesMap[group.id]); const isCollapsed = collapsedGroups[group.id]; return (
toggleGroup(group.id)}> {isCollapsed ? '▶' : '▼'}

{group.name}

{groupStats.total} resources {groupStats.uptime}% up {groupStats.avgResponse}ms avg
{!isCollapsed && (
{groupResourcesMap[group.id]?.map(renderResourceCard)}
)}
); })} {groupResourcesMap['ungrouped']?.length > 0 && (
toggleGroup('ungrouped')}> {collapsedGroups['ungrouped'] ? '▶' : '▼'}

Ungrouped

{groupResourcesMap['ungrouped'].length} resources {getGroupStats(groupResourcesMap['ungrouped']).uptime}% up {getGroupStats(groupResourcesMap['ungrouped']).avgResponse}ms avg
{!collapsedGroups['ungrouped'] && (
{groupResourcesMap['ungrouped'].map(renderResourceCard)}
)}
)}
)} {showGroupModal && (
setShowGroupModal(false)}>
e.stopPropagation()}>

Create New Group

setGroupData({ ...groupData, name: e.target.value })} required placeholder="e.g., Production Servers" />
setGroupData({ ...groupData, description: e.target.value })} placeholder="Optional description" />
)} {showModal && (
setShowModal(false)}>
e.stopPropagation()}>

Add New Resource

setFormData({ ...formData, name: e.target.value })} required placeholder="e.g., ZimaOS Home Server" />
setFormData({ ...formData, url: e.target.value })} required placeholder="https://your-zima.example.com" />
setFormData({ ...formData, check_interval: parseInt(e.target.value) })} min="10000" />
setFormData({ ...formData, timeout: parseInt(e.target.value) })} min="1000" />
{(formData.type === 'http' || formData.type === 'https' || formData.type === 'health') && ( <>
setFormData({ ...formData, http_keyword: e.target.value })} placeholder="Text that must appear in response" />