| import React, { useState, useEffect } from "react"; |
| import { DBService } from "../lib/db"; |
| import { WebsiteMonitor } from "../types"; |
| import { |
| Activity, Plus, Trash, Play, AlertTriangle, |
| CheckCircle, ArrowUpRight, TrendingUp, Clock, Shuffle |
| } from "lucide-react"; |
|
|
| export default function MonitorTab({ triggerHaptic }: { triggerHaptic: () => void }) { |
| const [monitors, setMonitors] = useState<WebsiteMonitor[]>([]); |
| const [name, setName] = useState(""); |
| const [url, setUrl] = useState(""); |
| const [interval, setIntervalVal] = useState<"30s" | "1m" | "5m" | "15m" | "1h">("1m"); |
| const [testingId, setTestingId] = useState<string | null>(null); |
|
|
| useEffect(() => { |
| loadMonitors(); |
| |
| |
| const intervalId = window.setInterval(() => { |
| runAutoPingSimulation(); |
| }, 15000); |
|
|
| return () => clearInterval(intervalId); |
| }, []); |
|
|
| const loadMonitors = async () => { |
| const list = await DBService.getAll<WebsiteMonitor>("monitors"); |
| |
| |
| const migratedList = list.map(m => { |
| if (m.url && m.url.includes("httpstat.us")) { |
| return { |
| ...m, |
| url: m.url.replace("httpstat.us/404", "httpbin.org/status/404").replace("httpstat.us", "httpbin.org") |
| }; |
| } |
| return m; |
| }); |
|
|
| |
| let wasMigrated = false; |
| for (let i = 0; i < list.length; i++) { |
| if (list[i].url !== migratedList[i].url) { |
| await DBService.put("monitors", migratedList[i]); |
| wasMigrated = true; |
| } |
| } |
|
|
| if (migratedList.length === 0) { |
| |
| const seed: WebsiteMonitor[] = [ |
| { |
| id: "mon_1", |
| name: "Vercel Build Edge", |
| url: "https://vercel.com", |
| interval: "1m", |
| status: "up", |
| lastCheck: Date.now() - 60000, |
| responseTime: 182, |
| history: [ |
| { timestamp: Date.now() - 300000, status: "up", responseTime: 190, code: 200 }, |
| { timestamp: Date.now() - 240000, status: "up", responseTime: 212, code: 200 }, |
| { timestamp: Date.now() - 180000, status: "up", responseTime: 180, code: 200 }, |
| { timestamp: Date.now() - 120000, status: "up", responseTime: 185, code: 200 }, |
| { timestamp: Date.now() - 60000, status: "up", responseTime: 182, code: 200 } |
| ] |
| }, |
| { |
| id: "mon_2", |
| name: "قاعدة بيانات Supabase API", |
| url: "https://supabase.com", |
| interval: "5m", |
| status: "up", |
| lastCheck: Date.now() - 120000, |
| responseTime: 295, |
| history: [ |
| { timestamp: Date.now() - 600000, status: "up", responseTime: 310, code: 200 }, |
| { timestamp: Date.now() - 480000, status: "up", responseTime: 302, code: 200 }, |
| { timestamp: Date.now() - 360000, status: "up", responseTime: 298, code: 200 }, |
| { timestamp: Date.now() - 240000, status: "up", responseTime: 290, code: 200 }, |
| { timestamp: Date.now() - 120000, status: "up", responseTime: 295, code: 200 } |
| ] |
| }, |
| { |
| id: "mon_3", |
| name: "API الخادم الاحتياطي", |
| url: "https://httpbin.org/status/404", |
| interval: "15m", |
| status: "down", |
| lastCheck: Date.now() - 300000, |
| responseTime: 1540, |
| history: [ |
| { timestamp: Date.now() - 1200000, status: "down", responseTime: 1200, code: 404 }, |
| { timestamp: Date.now() - 900000, status: "up", responseTime: 420, code: 200 }, |
| { timestamp: Date.now() - 600000, status: "down", responseTime: 1500, code: 504 }, |
| { timestamp: Date.now() - 300000, status: "down", responseTime: 1540, code: 404 } |
| ] |
| } |
| ]; |
| for (const m of seed) { |
| await DBService.put("monitors", m); |
| } |
| setMonitors(seed); |
| } else { |
| setMonitors(migratedList); |
| } |
| }; |
|
|
| const runAutoPingSimulation = async () => { |
| const list = await DBService.getAll<WebsiteMonitor>("monitors"); |
| if (list.length === 0) return; |
|
|
| const randomIndex = Math.floor(Math.random() * list.length); |
| const monitor = list[randomIndex]; |
| if (monitor) { |
| await executeSinglePing(monitor); |
| } |
| }; |
|
|
| const executeSinglePing = async (m: WebsiteMonitor) => { |
| setTestingId(m.id); |
| const start = performance.now(); |
| let status: "up" | "down" | "slow" = "up"; |
| let code = 200; |
| let respTime = 0; |
|
|
| try { |
| const formatUrl = m.url.startsWith("http") ? m.url : `https://${m.url}`; |
| const res = await fetch(`/api/proxy?url=${encodeURIComponent(formatUrl)}`, { |
| method: "GET" |
| }); |
| const end = performance.now(); |
| respTime = Math.round(end - start); |
| code = res.status; |
|
|
| if (!res.ok) { |
| status = "down"; |
| } else if (respTime > 800) { |
| status = "slow"; |
| } |
| } catch { |
| status = "down"; |
| code = 500; |
| respTime = 2000; |
| } |
|
|
| const updatedHistoryItem = { |
| timestamp: Date.now(), |
| status, |
| responseTime: respTime, |
| code |
| }; |
|
|
| const newHistory = [updatedHistoryItem, ...m.history].slice(0, 100); |
| const updated: WebsiteMonitor = { |
| ...m, |
| status, |
| lastCheck: Date.now(), |
| responseTime: respTime, |
| history: newHistory |
| }; |
|
|
| await DBService.put("monitors", updated); |
| |
| if (m.status !== status) { |
| await DBService.put("auditLog", { |
| id: "log_" + Date.now(), |
| timestamp: Date.now(), |
| action: "تنبيه مراقبة المواقع", |
| details: `مراقب المواقع [${m.name}] تغيرت حالته من [${m.status}] إلى [${status}]`, |
| status: status === "up" ? "success" : "danger" |
| }); |
| } |
|
|
| setMonitors(prev => prev.map(item => item.id === m.id ? updated : item)); |
| setTestingId(null); |
| }; |
|
|
| const handleAddMonitor = async (e: React.FormEvent) => { |
| e.preventDefault(); |
| if (!name || !url) return; |
| triggerHaptic(); |
|
|
| const newMonitor: WebsiteMonitor = { |
| id: "mon_" + Date.now(), |
| name, |
| url, |
| interval, |
| status: "unknown", |
| history: [] |
| }; |
|
|
| await DBService.put("monitors", newMonitor); |
| setMonitors(prev => [newMonitor, ...prev]); |
| |
| setName(""); |
| setUrl(""); |
| |
| executeSinglePing(newMonitor); |
| }; |
|
|
| const handleDelete = async (id: string) => { |
| triggerHaptic(); |
| await DBService.delete("monitors", id); |
| setMonitors(prev => prev.filter(m => m.id !== id)); |
| }; |
|
|
| return ( |
| <div className="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in text-slate-800"> |
| {/* Sidebar Add (Left) */} |
| <div className="lg:col-span-4 space-y-6"> |
| <form onSubmit={handleAddMonitor} className="glass-card bg-white/75 border border-sky-100 p-6 rounded-2xl space-y-4"> |
| <h2 className="text-sm font-extrabold text-sky-800 flex items-center gap-1.5"> |
| <Plus className="w-5 h-5 text-sky-600" /> |
| إضافة مراقب تفاعلي جديد |
| </h2> |
| |
| <div> |
| <label className="block text-xs text-slate-600 mb-1 font-semibold">اسم المراقب أو الخدمة</label> |
| <input |
| type="text" |
| placeholder="مثال: الخادم الرئيسي" |
| required |
| value={name} |
| onChange={(e) => setName(e.target.value)} |
| className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full font-bold" |
| /> |
| </div> |
| |
| <div> |
| <label className="block text-xs text-slate-600 mb-1 font-semibold">الرابط أو عنوان الـ API</label> |
| <input |
| type="text" |
| placeholder="api.example.com" |
| required |
| value={url} |
| onChange={(e) => setUrl(e.target.value)} |
| className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full text-left font-mono" |
| /> |
| </div> |
| |
| <div> |
| <label className="block text-xs text-slate-600 mb-1 font-semibold">معدل الفحص الدوري</label> |
| <select |
| value={interval} |
| onChange={(e) => setIntervalVal(e.target.value as any)} |
| className="bg-white border border-sky-200 rounded-xl px-4 py-2 text-xs text-slate-800 focus:outline-none focus:border-sky-500 w-full font-bold" |
| > |
| <option value="30s">كل 30 ثانية</option> |
| <option value="1m">كل دقيقة واحدة</option> |
| <option value="5m">كل 5 دقائق</option> |
| <option value="15m">كل 15 دقيقة</option> |
| <option value="1h">كل ساعة</option> |
| </select> |
| </div> |
| |
| <button |
| type="submit" |
| className="w-full bg-sky-600 hover:bg-sky-700 text-white font-bold py-2.5 rounded-xl text-xs flex items-center justify-center gap-1.5 transition shadow-sm cursor-pointer" |
| > |
| <Activity className="w-4 h-4" /> |
| بدء المراقبة النشطة |
| </button> |
| </form> |
| |
| {/* Global Stats */} |
| <div className="glass-card bg-white/75 border border-sky-100 p-6 rounded-2xl relative overflow-hidden"> |
| <div className="absolute top-0 right-0 w-32 h-32 bg-sky-500/5 blur-2xl rounded-full" /> |
| <h3 className="text-xs font-extrabold text-sky-800 mb-4 flex items-center gap-1.5"> |
| <TrendingUp className="w-4 h-4 text-sky-600" /> |
| ملخص الأداء والحيّوية |
| </h3> |
| <div className="space-y-4"> |
| <div className="flex justify-between items-center text-xs"> |
| <span className="text-slate-600 font-bold">العدد الكلي للمراقبين:</span> |
| <span className="font-extrabold text-sky-900 font-mono">{monitors.length} قنوات</span> |
| </div> |
| <div className="flex justify-between items-center text-xs"> |
| <span className="text-slate-600 font-bold">الخوادم النشطة (Up):</span> |
| <span className="font-black text-emerald-600 font-mono"> |
| {monitors.filter(m => m.status === "up").length} |
| </span> |
| </div> |
| <div className="flex justify-between items-center text-xs"> |
| <span className="text-slate-600 font-bold">الخوادم المتوقفة (Down):</span> |
| <span className="font-black text-red-500 font-mono"> |
| {monitors.filter(m => m.status === "down").length} |
| </span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Monitors Dashboard View (Right) */} |
| <div className="lg:col-span-8 space-y-4"> |
| {monitors.map((m) => ( |
| <div key={m.id} className="glass-card bg-white/75 border border-sky-100 p-5 rounded-2xl flex flex-col md:flex-row justify-between gap-4 relative overflow-hidden"> |
| <div className={`absolute top-0 right-0 w-1.5 h-full ${ |
| m.status === "up" ? "bg-emerald-500" : m.status === "down" ? "bg-red-500" : "bg-yellow-500" |
| }`} /> |
| |
| {/* Title Block */} |
| <div className="flex-1 space-y-2 pr-2"> |
| <div className="flex items-center gap-2 flex-wrap"> |
| <h3 className="font-extrabold text-sm text-sky-900">{m.name}</h3> |
| <span className={`px-2 py-0.5 rounded text-[10px] font-bold ${ |
| m.status === "up" |
| ? "bg-emerald-100 text-emerald-700 border border-emerald-200" |
| : m.status === "down" |
| ? "bg-red-100 text-red-700 border border-red-200" |
| : "bg-sky-100 text-sky-700" |
| }`}> |
| {m.status === "up" ? "متصل (UP)" : m.status === "down" ? "غير متصل (DOWN)" : "قيد الاختبار"} |
| </span> |
| <span className="text-[10px] text-slate-500 flex items-center gap-0.5"> |
| <Clock className="w-3 h-3 text-sky-600" /> |
| {m.interval} |
| </span> |
| </div> |
| <p className="text-xs text-slate-600 text-left font-mono truncate">{m.url}</p> |
| |
| <div className="flex items-center gap-1.5 pt-1"> |
| <span className="text-[10px] text-slate-500 font-bold">سجل الأحداث المباشر:</span> |
| <div className="flex gap-1"> |
| {m.history.length === 0 ? ( |
| <span className="text-[10px] text-slate-500">لا يوجد سجل تاريخي</span> |
| ) : ( |
| m.history.map((h, index) => ( |
| <div |
| key={index} |
| title={`تاريخ الفحص: ${new Date(h.timestamp).toLocaleTimeString("ar-EG")} - الاستجابة: ${h.responseTime}ms - الحالة: ${h.status}`} |
| className={`w-3.5 h-3.5 rounded-full flex items-center justify-center text-[8px] font-bold border ${ |
| h.status === "up" |
| ? "bg-emerald-100 text-emerald-700 border-emerald-300" |
| : h.status === "down" |
| ? "bg-red-100 text-red-700 border-red-300" |
| : "bg-yellow-100 text-yellow-700 border-yellow-300" |
| }`} |
| > |
| • |
| </div> |
| )) |
| )} |
| </div> |
| </div> |
| </div> |
| |
| {/* Telemetry Actions Block */} |
| <div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between md:justify-end gap-3 shrink-0"> |
| <div className="bg-sky-50 border border-sky-150 rounded-xl p-3 text-center sm:text-right min-w-[120px]"> |
| <span className="text-[10px] text-slate-500 block mb-0.5">سرعة الاستجابة</span> |
| {m.responseTime ? ( |
| <span className="text-sm font-black text-sky-850 font-mono"> |
| {m.responseTime} <span className="text-[10px]">ms</span> |
| </span> |
| ) : ( |
| <span className="text-xs text-slate-500 font-bold">--</span> |
| )} |
| </div> |
| |
| {/* Run Test & Delete */} |
| <div className="flex gap-2"> |
| <button |
| onClick={() => executeSinglePing(m)} |
| disabled={testingId === m.id} |
| className="flex-1 sm:flex-initial px-3.5 py-2.5 bg-sky-100 hover:bg-sky-200 disabled:opacity-50 text-sky-800 border border-sky-200 rounded-xl transition flex items-center justify-center gap-1.5 text-xs font-bold cursor-pointer" |
| > |
| {testingId === m.id ? ( |
| <Shuffle className="w-3.5 h-3.5 animate-spin text-sky-600" /> |
| ) : ( |
| <Play className="w-3.5 h-3.5 text-sky-600" /> |
| )} |
| فحص الآن |
| </button> |
| <button |
| onClick={() => handleDelete(m.id)} |
| className="p-2.5 bg-white hover:bg-red-50 text-red-500 border border-sky-250/20 rounded-xl transition cursor-pointer" |
| > |
| <Trash className="w-3.5 h-3.5" /> |
| </button> |
| </div> |
| </div> |
| </div> |
| ))} |
| |
| {monitors.length === 0 && ( |
| <div className="glass-card p-12 text-center text-slate-400 rounded-2xl bg-white/75 border border-sky-50"> |
| لا توجد مخدمات مراقبة نشطة مضافة بعد، أضف عنوان خدمة للبدء. |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| } |
|
|