"use client" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Badge } from "./ui/badge" import { Button } from "./ui/button" import { Input } from "./ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { ScrollArea } from "./ui/scroll-area" import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "./ui/sheet" import { FileText, Search, Download, AlertTriangle, Info, CheckCircle, XCircle, Database, Activity, HardDrive, Calendar, RefreshCw, Bell, Mail, Menu, Terminal, CalendarDays, } from "lucide-react" import { useState, useEffect } from "react" interface Log { timestamp: string level: string service: string message: string source: string pid?: string hostname?: string } interface Backup { volid: string storage: string vmid: string | null type: string | null size: number size_human: string created: string timestamp: number } interface Event { upid: string type: string status: string level: string node: string user: string vmid: string starttime: string endtime: string duration: string } interface Notification { timestamp: string type: string service: string message: string source: string } interface SystemLog { timestamp: string level: string service: string message: string source: string pid?: string hostname?: string } interface CombinedLogEntry { timestamp: string level: string service: string message: string source: string pid?: string hostname?: string isEvent: boolean eventData?: Event sortTimestamp: number } export function SystemLogs() { const [logs, setLogs] = useState([]) const [backups, setBackups] = useState([]) const [events, setEvents] = useState([]) const [notifications, setNotifications] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [searchTerm, setSearchTerm] = useState("") const [levelFilter, setLevelFilter] = useState("all") const [serviceFilter, setServiceFilter] = useState("all") const [activeTab, setActiveTab] = useState("logs") const [displayedLogsCount, setDisplayedLogsCount] = useState(500) const [selectedLog, setSelectedLog] = useState(null) const [selectedEvent, setSelectedEvent] = useState(null) const [selectedBackup, setSelectedBackup] = useState(null) const [selectedNotification, setSelectedNotification] = useState(null) // Added const [isLogModalOpen, setIsLogModalOpen] = useState(false) const [isEventModalOpen, setIsEventModalOpen] = useState(false) const [isBackupModalOpen, setIsBackupModalOpen] = useState(false) const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false) // Added const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) const [dateFilter, setDateFilter] = useState("now") const [customDays, setCustomDays] = useState("1") const getApiUrl = (endpoint: string) => { if (typeof window !== "undefined") { return `${window.location.protocol}//${window.location.hostname}:8008${endpoint}` } return `http://localhost:8008${endpoint}` } useEffect(() => { fetchAllData() }, []) useEffect(() => { console.log("[v0] Date filter changed:", dateFilter, "Custom days:", customDays) if (dateFilter !== "now") { setLoading(true) fetchSystemLogs() .then((newLogs) => { console.log("[v0] Loaded logs for date filter:", dateFilter, "Count:", newLogs.length) console.log("[v0] First log:", newLogs[0]) setLogs(newLogs) setLoading(false) }) .catch((err) => { console.error("[v0] Error loading logs:", err) setLoading(false) }) } else { fetchAllData() } }, [dateFilter, customDays]) useEffect(() => { console.log("[v0] Level or service filter changed:", levelFilter, serviceFilter) if (levelFilter !== "all" || serviceFilter !== "all") { setLoading(true) fetchSystemLogs() .then((newLogs) => { console.log( "[v0] Loaded logs for filters - Level:", levelFilter, "Service:", serviceFilter, "Count:", newLogs.length, ) setLogs(newLogs) setLoading(false) }) .catch((err) => { console.error("[v0] Error loading logs:", err) setLoading(false) }) } else if (dateFilter === "now") { // Only reload all data if we're on "now" and all filters are cleared fetchAllData() } }, [levelFilter, serviceFilter]) const fetchAllData = async () => { try { setLoading(true) setError(null) const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([ fetchSystemLogs(), fetch(getApiUrl("/api/backups")), fetch(getApiUrl("/api/events?limit=50")), fetch(getApiUrl("/api/notifications")), ]) setLogs(logsRes) if (backupsRes.ok) { const backupsData = await backupsRes.json() setBackups(backupsData.backups || []) } if (eventsRes.ok) { const eventsData = await eventsRes.json() setEvents(eventsData.events || []) } if (notificationsRes.ok) { const notificationsData = await notificationsRes.json() setNotifications(notificationsData.notifications || []) } } catch (err) { console.error("[v0] Error fetching system logs data:", err) setError("Failed to connect to server") } finally { setLoading(false) } } const fetchSystemLogs = async (): Promise => { try { let apiUrl = getApiUrl("/api/logs") const params = new URLSearchParams() if (dateFilter !== "now") { const daysAgo = dateFilter === "custom" ? Number.parseInt(customDays) : Number.parseInt(dateFilter) params.append("since_days", daysAgo.toString()) console.log("[v0] Fetching logs since_days:", daysAgo) } if (levelFilter !== "all") { const priorityMap: Record = { error: "3", // 0-3: emerg, alert, crit, err warning: "4", // 4: warning info: "6", // 5-7: notice, info, debug } const priority = priorityMap[levelFilter] if (priority) { params.append("priority", priority) console.log("[v0] Fetching logs with priority:", priority, "for level:", levelFilter) } } if (serviceFilter !== "all") { params.append("service", serviceFilter) console.log("[v0] Fetching logs for service:", serviceFilter) } params.append("limit", "5000") if (params.toString()) { apiUrl += `?${params.toString()}` } console.log("[v0] Making fetch request to:", apiUrl) const response = await fetch(apiUrl, { method: "GET", headers: { "Content-Type": "application/json", }, cache: "no-store", signal: AbortSignal.timeout(30000), // 30 second timeout }) console.log("[v0] Response status:", response.status, "OK:", response.ok) if (!response.ok) { throw new Error(`Flask server responded with status: ${response.status}`) } const data = await response.json() console.log("[v0] Received logs data, count:", data.logs?.length || 0) const logsArray = Array.isArray(data) ? data : data.logs || [] console.log("[v0] Returning logs array with length:", logsArray.length) return logsArray } catch (error) { console.error("[v0] Failed to fetch system logs:", error) if (error instanceof Error && error.name === "TimeoutError") { setError("Request timed out. Try selecting a more specific filter.") } else { setError("Failed to load logs. Please try again.") } return [] } } const handleDownloadLogs = async () => { try { // Generate filename based on active filters const filters = [] if (dateFilter !== "now") { const days = dateFilter === "custom" ? customDays : dateFilter filters.push(`${days}days`) } if (levelFilter !== "all") { filters.push(levelFilter) } if (serviceFilter !== "all") { filters.push(serviceFilter) } if (searchTerm) { filters.push("searched") } const filename = `proxmox_logs_${filters.length > 0 ? filters.join("_") + "_" : ""}${new Date().toISOString().split("T")[0]}.txt` // Generate log content const logContent = [ `Proxmox System Logs & Events Export`, `Generated: ${new Date().toISOString()}`, `Total Entries: ${filteredCombinedLogs.length.toLocaleString()}`, ``, `Filters Applied:`, `- Date Range: ${dateFilter === "now" ? "Current logs" : dateFilter === "custom" ? `${customDays} days ago` : `${dateFilter} days ago`}`, `- Level: ${levelFilter === "all" ? "All Levels" : levelFilter}`, `- Service: ${serviceFilter === "all" ? "All Services" : serviceFilter}`, `- Search: ${searchTerm || "None"}`, ``, `${"=".repeat(80)}`, ``, ...filteredCombinedLogs.map((log) => { const lines = [ `[${log.timestamp}] ${log.level.toUpperCase()} - ${log.service}${log.isEvent ? " [EVENT]" : ""}`, `Message: ${log.message}`, `Source: ${log.source}`, ] if (log.pid) lines.push(`PID: ${log.pid}`) if (log.hostname) lines.push(`Hostname: ${log.hostname}`) lines.push(`${"-".repeat(80)}`) return lines.join("\n") }), ].join("\n") // Create and download blob const blob = new Blob([logContent], { type: "text/plain" }) const url = window.URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = filename document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) } catch (err) { console.error("Error exporting logs:", err) } } const extractUPID = (message: string): string | null => { const upidMatch = message.match(/UPID:[^\s:]+:[^\s:]+:[^\s:]+:[^\s:]+:[^\s:]+:[^\s:]*:[^\s:]*:?[^\s]*/) return upidMatch ? upidMatch[0] : null } const handleDownloadNotificationLog = async (notification: Notification) => { try { const upid = extractUPID(notification.message) if (upid) { // Try to fetch the complete task log from Proxmox try { const response = await fetch(getApiUrl(`/api/task-log/${encodeURIComponent(upid)}`)) if (response.ok) { const taskLog = await response.text() // Download the complete task log const blob = new Blob( [ `Proxmox Task Log\n`, `================\n\n`, `UPID: ${upid}\n`, `Timestamp: ${notification.timestamp}\n`, `Service: ${notification.service}\n`, `Source: ${notification.source}\n\n`, `Complete Task Log:\n`, `${"-".repeat(80)}\n`, `${taskLog}\n`, ], { type: "text/plain" }, ) const url = window.URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = `task_log_${upid.replace(/:/g, "_")}_${notification.timestamp.replace(/[:\s]/g, "_")}.txt` document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) return } } catch (error) { console.error("[v0] Failed to fetch task log from Proxmox:", error) // Fall through to download notification message } } // If no UPID or failed to fetch task log, download the notification message const blob = new Blob( [ `Notification Details\n`, `==================\n\n`, `Timestamp: ${notification.timestamp}\n`, `Type: ${notification.type}\n`, `Service: ${notification.service}\n`, `Source: ${notification.source}\n\n`, `Complete Message:\n`, `${notification.message}\n`, ], { type: "text/plain" }, ) const url = window.URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = `notification_${notification.timestamp.replace(/[:\s]/g, "_")}.txt` document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) } catch (err) { console.error("[v0] Error downloading notification:", err) } } const combinedLogs: CombinedLogEntry[] = [ ...logs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })), ...events.map((event) => ({ timestamp: event.starttime, level: event.level, service: event.type, message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`, source: `Node: ${event.node} • User: ${event.user}`, isEvent: true, eventData: event, sortTimestamp: new Date(event.starttime).getTime(), })), ].sort((a, b) => b.sortTimestamp - a.sortTimestamp) // Sort by timestamp descending // Filter combined logs const filteredCombinedLogs = combinedLogs.filter((log) => { const matchesSearch = log.message.toLowerCase().includes(searchTerm.toLowerCase()) || log.service.toLowerCase().includes(searchTerm.toLowerCase()) const matchesLevel = levelFilter === "all" || log.level === levelFilter const matchesService = serviceFilter === "all" || log.service === serviceFilter return matchesSearch && matchesLevel && matchesService }) const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount) const hasMoreLogs = displayedLogsCount < filteredCombinedLogs.length const getLevelColor = (level: string) => { switch (level) { case "error": case "critical": case "emergency": case "alert": return "bg-red-500/10 text-red-500 border-red-500/20" case "warning": return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" case "info": case "notice": return "bg-blue-500/10 text-blue-500 border-blue-500/20" case "success": return "bg-green-500/10 text-green-500 border-green-500/20" default: return "bg-gray-500/10 text-gray-500 border-gray-500/20" } } const getLevelIcon = (level: string) => { switch (level) { case "error": case "critical": case "emergency": case "alert": return case "warning": return case "info": case "notice": return case "success": return default: return } } const getNotificationIcon = (type: string) => { switch (type) { case "email": return case "webhook": return case "alert": return case "error": return case "success": return default: return } } const getNotificationTypeColor = (type: string) => { switch (type.toLowerCase()) { case "error": return "bg-red-500/10 text-red-500 border-red-500/20" case "warning": return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" case "info": return "bg-blue-500/10 text-blue-500 border-blue-500/20" case "success": return "bg-green-500/10 text-green-500 border-green-500/20" default: return "bg-gray-500/10 text-gray-500 border-gray-500/20" } } // ADDED: New function for notification source colors const getNotificationSourceColor = (source: string) => { switch (source.toLowerCase()) { case "task-log": return "bg-purple-500/10 text-purple-500 border-purple-500/20" case "journal": return "bg-cyan-500/10 text-cyan-500 border-cyan-500/20" case "system": return "bg-orange-500/10 text-orange-500 border-orange-500/20" default: return "bg-gray-500/10 text-gray-500 border-gray-500/20" } } const logCounts = { total: logs.length, error: logs.filter((log) => ["error", "critical", "emergency", "alert"].includes(log.level)).length, warning: logs.filter((log) => log.level === "warning").length, info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length, } const uniqueServices = [...new Set(logs.map((log) => log.service))] const getBackupType = (volid: string): "vm" | "lxc" => { if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) { return "vm" } return "lxc" } const getBackupTypeColor = (volid: string) => { const type = getBackupType(volid) return type === "vm" ? "bg-cyan-500/10 text-cyan-500 border-cyan-500/20" : "bg-orange-500/10 text-orange-500 border-orange-500/20" } const getBackupTypeLabel = (volid: string) => { const type = getBackupType(volid) return type === "vm" ? "VM" : "LXC" } const getBackupStorageType = (volid: string): "pbs" | "pve" => { // PBS backups have format: storage:backup/type/vmid/timestamp // PVE backups have format: storage:backup/vzdump-type-vmid-timestamp.vma.zst if (volid.includes(":backup/vm/") || volid.includes(":backup/ct/")) { return "pbs" } return "pve" } const getBackupStorageColor = (volid: string) => { const type = getBackupStorageType(volid) return type === "pbs" ? "bg-purple-500/10 text-purple-500 border-purple-500/20" : "bg-blue-500/10 text-blue-500 border-blue-500/20" } const getBackupStorageLabel = (volid: string) => { const type = getBackupStorageType(volid) return type === "pbs" ? "PBS" : "PVE" } const backupStats = { total: backups.length, totalSize: backups.reduce((sum, b) => sum + b.size, 0), qemu: backups.filter((b) => { // Check if volid contains /vm/ for QEMU or vzdump-qemu for PVE return b.volid.includes("/vm/") || b.volid.includes("vzdump-qemu") }).length, lxc: backups.filter((b) => { // Check if volid contains /ct/ for LXC or vzdump-lxc for PVE return b.volid.includes("/ct/") || b.volid.includes("vzdump-lxc") }).length, } const formatBytes = (bytes: number) => { if (bytes === 0) return "0 B" const k = 1024 const sizes = ["B", "KB", "MB", "GB", "TB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}` } const getSectionIcon = (section: string) => { switch (section) { case "logs": return case "events": return case "backups": return case "notifications": return default: return } } const getSectionLabel = (section: string) => { switch (section) { case "logs": return "Logs & Events" case "events": return "Recent Events" case "backups": return "Backups" case "notifications": return "Notifications" default: return section } } if (loading && logs.length === 0 && events.length === 0) { return (
) } return (
{loading && (logs.length > 0 || events.length > 0) && (
Loading logs selected...
Please wait while we fetch the logs
)} {/* Statistics Cards */}
Total Entries
{filteredCombinedLogs.length.toLocaleString("fr-FR")}

Filtered

Errors
{logCounts.error.toLocaleString("fr-FR")}

Requires attention

Warnings
{logCounts.warning.toLocaleString("fr-FR")}

Monitor closely

Backups
{backupStats.total.toLocaleString("fr-FR")}

{formatBytes(backupStats.totalSize)}

{/* Main Content with Tabs */}
System Logs & Events
Logs & Events Backups Notifications
Sections
setSearchTerm(e.target.value)} className="pl-10 bg-background border-border" />
{dateFilter === "custom" && ( setCustomDays(e.target.value)} className="w-full sm:w-[120px] bg-background border-border" min="1" /> )}
{displayedLogs.map((log, index) => (
{ if (log.isEvent) { setSelectedEvent(log.eventData) setIsEventModalOpen(true) } else { setSelectedLog(log as SystemLog) // Cast to SystemLog for dialog setIsLogModalOpen(true) } }} >
{getLevelIcon(log.level)} {log.level.toUpperCase()} {log.isEvent && ( EVENT )}
{log.service}
{log.timestamp}
{log.message}
{log.source} {log.pid && ` • PID: ${log.pid}`} {log.hostname && ` • Host: ${log.hostname}`}
))} {displayedLogs.length === 0 && (

No logs or events found matching your criteria

)} {hasMoreLogs && (
)}
{/* Backups Tab */}
{backupStats.qemu}

QEMU Backups

{backupStats.lxc}

LXC Backups

{formatBytes(backupStats.totalSize)}

Total Size

{backups.map((backup, index) => (
{ setSelectedBackup(backup) setIsBackupModalOpen(true) }} >
{getBackupTypeLabel(backup.volid)} {getBackupStorageLabel(backup.volid)}
{backup.size_human}
Storage: {backup.storage}
{backup.created}
))} {backups.length === 0 && (

No backups found

)}
{/* Notifications Tab */}
{notifications.map((notification, index) => (
{ setSelectedNotification(notification) setIsNotificationModalOpen(true) }} >
{notification.type.toUpperCase()} {notification.source === "task-log" && } {notification.source === "journal" && } {notification.source.toUpperCase()}
{notification.service}
{notification.timestamp}
{notification.message}
Service: {notification.service} • Source: {notification.source}
))} {notifications.length === 0 && (

No notifications found

)}
Log Details Complete information about this log entry {selectedLog && (
Level
{getLevelIcon(selectedLog.level)} {selectedLog.level.toUpperCase()}
Service
{selectedLog.service}
Timestamp
{selectedLog.timestamp}
Source
{selectedLog.source}
{selectedLog.pid && (
Process ID
{selectedLog.pid}
)} {selectedLog.hostname && (
Hostname
{selectedLog.hostname}
)}
Message
{selectedLog.message}
)}
Event Details Complete information about this event {selectedEvent && (
{getLevelIcon(selectedEvent.level)} {selectedEvent.level.toUpperCase()} EVENT
)}
{selectedEvent && (
Message
{selectedEvent.status}
Type
{selectedEvent.type}
Node
{selectedEvent.node}
User
{selectedEvent.user}
{selectedEvent.vmid && (
VM/CT ID
{selectedEvent.vmid}
)}
Duration
{selectedEvent.duration}
Start Time
{selectedEvent.starttime}
End Time
{selectedEvent.endtime}
UPID
                    {selectedEvent.upid}
                  
)}
Backup Details Complete information about this backup {selectedBackup && (
Type
{getBackupTypeLabel(selectedBackup.volid)}
Storage Type
{getBackupStorageLabel(selectedBackup.volid)}
Storage
{selectedBackup.storage}
Size
{selectedBackup.size_human}
{selectedBackup.vmid && (
VM/CT ID
{selectedBackup.vmid}
)}
Created
{selectedBackup.created}
Volume ID
                    {selectedBackup.volid}
                  
)}
Notification Details Complete information about this notification {selectedNotification && (
Type
{selectedNotification.type.toUpperCase()}
Timestamp
{selectedNotification.timestamp}
Service
{selectedNotification.service}
Source
{selectedNotification.source}
Message
                    {selectedNotification.message}
                  
)}
) }