mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update Log
This commit is contained in:
@@ -30,16 +30,6 @@ import {
|
|||||||
import { useState, useEffect, useMemo } from "react"
|
import { useState, useEffect, useMemo } from "react"
|
||||||
import { API_PORT, fetchApi } from "@/lib/api-config"
|
import { API_PORT, fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
interface Log {
|
|
||||||
timestamp: string
|
|
||||||
level: string
|
|
||||||
service: string
|
|
||||||
message: string
|
|
||||||
source: string
|
|
||||||
pid?: string
|
|
||||||
hostname?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Backup {
|
interface Backup {
|
||||||
volid: string
|
volid: string
|
||||||
storage: string
|
storage: string
|
||||||
@@ -76,6 +66,7 @@ interface SystemLog {
|
|||||||
timestamp: string
|
timestamp: string
|
||||||
level: string
|
level: string
|
||||||
service: string
|
service: string
|
||||||
|
unit?: string
|
||||||
message: string
|
message: string
|
||||||
source: string
|
source: string
|
||||||
pid?: string
|
pid?: string
|
||||||
@@ -86,6 +77,7 @@ interface CombinedLogEntry {
|
|||||||
timestamp: string
|
timestamp: string
|
||||||
level: string
|
level: string
|
||||||
service: string
|
service: string
|
||||||
|
unit?: string
|
||||||
message: string
|
message: string
|
||||||
source: string
|
source: string
|
||||||
pid?: string
|
pid?: string
|
||||||
@@ -108,111 +100,60 @@ export function SystemLogs() {
|
|||||||
const [serviceFilter, setServiceFilter] = useState("all")
|
const [serviceFilter, setServiceFilter] = useState("all")
|
||||||
const [activeTab, setActiveTab] = useState("logs")
|
const [activeTab, setActiveTab] = useState("logs")
|
||||||
|
|
||||||
const [displayedLogsCount, setDisplayedLogsCount] = useState(50) // Increased from 500 to 50 for initial load, will use pagination
|
const [displayedLogsCount, setDisplayedLogsCount] = useState(100)
|
||||||
|
|
||||||
const [selectedLog, setSelectedLog] = useState<SystemLog | null>(null)
|
const [selectedLog, setSelectedLog] = useState<SystemLog | null>(null)
|
||||||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null)
|
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null)
|
||||||
const [selectedBackup, setSelectedBackup] = useState<Backup | null>(null)
|
const [selectedBackup, setSelectedBackup] = useState<Backup | null>(null)
|
||||||
const [selectedNotification, setSelectedNotification] = useState<Notification | null>(null) // Added
|
const [selectedNotification, setSelectedNotification] = useState<Notification | null>(null)
|
||||||
const [isLogModalOpen, setIsLogModalOpen] = useState(false)
|
const [isLogModalOpen, setIsLogModalOpen] = useState(false)
|
||||||
const [isEventModalOpen, setIsEventModalOpen] = useState(false)
|
const [isEventModalOpen, setIsEventModalOpen] = useState(false)
|
||||||
const [isBackupModalOpen, setIsBackupModalOpen] = useState(false)
|
const [isBackupModalOpen, setIsBackupModalOpen] = useState(false)
|
||||||
const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false) // Added
|
const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false)
|
||||||
|
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||||
|
|
||||||
const [dateFilter, setDateFilter] = useState("1") // Changed from "now" to "1" to load 1 day by default
|
const [dateFilter, setDateFilter] = useState("1")
|
||||||
const [customDays, setCustomDays] = useState("1")
|
const [customDays, setCustomDays] = useState("1")
|
||||||
|
const [refreshCounter, setRefreshCounter] = useState(0)
|
||||||
|
|
||||||
const getApiUrl = (endpoint: string) => {
|
// Single unified useEffect for all data loading
|
||||||
if (typeof window !== "undefined") {
|
// Fires on mount, when filters change, or when refresh is triggered
|
||||||
const { protocol, hostname, port } = window.location
|
|
||||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
|
||||||
|
|
||||||
if (isStandardPort) {
|
|
||||||
return endpoint
|
|
||||||
} else {
|
|
||||||
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This part might not be strictly necessary if only running client-side, but good for SSR safety
|
|
||||||
// In a real SSR scenario, you'd need to handle API_PORT differently
|
|
||||||
const protocol = typeof window !== "undefined" ? window.location.protocol : "http:" // Defaulting to http for SSR safety
|
|
||||||
const hostname = typeof window !== "undefined" ? window.location.hostname : "localhost" // Defaulting to localhost for SSR safety
|
|
||||||
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAllData()
|
let cancelled = false
|
||||||
}, [])
|
const loadData = async () => {
|
||||||
|
|
||||||
// CHANGE: Simplified useEffect - always fetch logs with date filter (no more "now" option)
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[v0] Date filter changed:", dateFilter, "Custom days:", customDays)
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}, [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 {
|
|
||||||
// Only reload all data if we're on "now" and all filters are cleared
|
|
||||||
// This else block is now theoretically unreachable given the change above, but kept for safety
|
|
||||||
fetchAllData()
|
|
||||||
}
|
|
||||||
}, [levelFilter, serviceFilter])
|
|
||||||
|
|
||||||
const fetchAllData = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
try {
|
||||||
const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([
|
const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([
|
||||||
fetchSystemLogs(),
|
fetchSystemLogs(),
|
||||||
fetchApi("/api/backups"),
|
fetchApi("/api/backups"),
|
||||||
fetchApi("/api/events?limit=50"),
|
fetchApi("/api/events?limit=50"),
|
||||||
fetchApi("/api/notifications"),
|
fetchApi("/api/notifications"),
|
||||||
])
|
])
|
||||||
|
if (cancelled) return
|
||||||
setLogs(logsRes)
|
setLogs(logsRes)
|
||||||
setBackups(backupsRes.backups || [])
|
setBackups(backupsRes.backups || [])
|
||||||
setEvents(eventsRes.events || [])
|
setEvents(eventsRes.events || [])
|
||||||
setNotifications(notificationsRes.notifications || [])
|
setNotifications(notificationsRes.notifications || [])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[v0] Error fetching system logs data:", err)
|
if (cancelled) return
|
||||||
setError("Failed to connect to server")
|
setError("Failed to connect to server")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
if (!cancelled) setLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
loadData()
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [dateFilter, customDays, refreshCounter])
|
||||||
|
|
||||||
|
// Reset pagination when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayedLogsCount(100)
|
||||||
|
}, [searchTerm, levelFilter, serviceFilter, dateFilter, customDays])
|
||||||
|
|
||||||
|
const refreshData = () => {
|
||||||
|
setRefreshCounter((prev) => prev + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
||||||
@@ -220,49 +161,21 @@ export function SystemLogs() {
|
|||||||
let apiUrl = "/api/logs"
|
let apiUrl = "/api/logs"
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
// CHANGE: Always add since_days parameter (no more "now" option)
|
|
||||||
const daysAgo = dateFilter === "custom" ? Number.parseInt(customDays) : Number.parseInt(dateFilter)
|
const daysAgo = dateFilter === "custom" ? Number.parseInt(customDays) : Number.parseInt(dateFilter)
|
||||||
params.append("since_days", daysAgo.toString())
|
// Clamp days to valid range
|
||||||
console.log("[v0] Fetching logs since_days:", daysAgo)
|
const clampedDays = Math.max(1, Math.min(daysAgo || 1, 90))
|
||||||
|
params.append("since_days", clampedDays.toString())
|
||||||
if (levelFilter !== "all") {
|
|
||||||
const priorityMap: Record<string, string> = {
|
|
||||||
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")
|
params.append("limit", "5000")
|
||||||
|
|
||||||
if (params.toString()) {
|
if (params.toString()) {
|
||||||
apiUrl += `?${params.toString()}`
|
apiUrl += `?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[v0] Making fetch request to:", apiUrl)
|
|
||||||
const data = await fetchApi(apiUrl)
|
const data = await fetchApi(apiUrl)
|
||||||
console.log("[v0] Received logs data, count:", data.logs?.length || 0)
|
|
||||||
|
|
||||||
const logsArray = Array.isArray(data) ? data : data.logs || []
|
const logsArray = Array.isArray(data) ? data : data.logs || []
|
||||||
console.log("[v0] Returning logs array with length:", logsArray.length)
|
|
||||||
return logsArray
|
return logsArray
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("[v0] Failed to fetch system logs:", error)
|
setError("Failed to load logs. Please try again.")
|
||||||
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 []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +184,6 @@ export function SystemLogs() {
|
|||||||
try {
|
try {
|
||||||
// Generate filename based on active filters
|
// Generate filename based on active filters
|
||||||
const filters = []
|
const filters = []
|
||||||
// CHANGE: Always include days in filename (no more "now" option)
|
|
||||||
const days = dateFilter === "custom" ? customDays : dateFilter
|
const days = dateFilter === "custom" ? customDays : dateFilter
|
||||||
filters.push(`${days}days`)
|
filters.push(`${days}days`)
|
||||||
|
|
||||||
@@ -294,7 +206,7 @@ export function SystemLogs() {
|
|||||||
`Total Entries: ${filteredCombinedLogs.length.toLocaleString()}`,
|
`Total Entries: ${filteredCombinedLogs.length.toLocaleString()}`,
|
||||||
``,
|
``,
|
||||||
`Filters Applied:`,
|
`Filters Applied:`,
|
||||||
`- Date Range: ${dateFilter === "now" ? "Current logs" : dateFilter === "custom" ? `${customDays} days ago` : `${dateFilter} days ago`}`,
|
`- Date Range: ${dateFilter === "custom" ? `${customDays} days ago` : `${dateFilter} day(s) ago`}`,
|
||||||
`- Level: ${levelFilter === "all" ? "All Levels" : levelFilter}`,
|
`- Level: ${levelFilter === "all" ? "All Levels" : levelFilter}`,
|
||||||
`- Service: ${serviceFilter === "all" ? "All Services" : serviceFilter}`,
|
`- Service: ${serviceFilter === "all" ? "All Services" : serviceFilter}`,
|
||||||
`- Search: ${searchTerm || "None"}`,
|
`- Search: ${searchTerm || "None"}`,
|
||||||
@@ -368,8 +280,7 @@ export function SystemLogs() {
|
|||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
return
|
return
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("[v0] Failed to fetch task log from Proxmox:", error)
|
|
||||||
// Fall through to download notification message
|
// Fall through to download notification message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,8 +308,8 @@ export function SystemLogs() {
|
|||||||
a.click()
|
a.click()
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.error("[v0] Error downloading notification:", err)
|
// Download failed silently
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,70 +318,11 @@ export function SystemLogs() {
|
|||||||
return String(value).toLowerCase()
|
return String(value).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
const memoizedLogs = useMemo(() => logs, [logs])
|
|
||||||
const memoizedEvents = useMemo(() => events, [events])
|
|
||||||
const memoizedBackups = useMemo(() => backups, [backups])
|
|
||||||
const memoizedNotifications = useMemo(() => notifications, [notifications])
|
|
||||||
|
|
||||||
const logsOnly: CombinedLogEntry[] = useMemo(
|
|
||||||
() =>
|
|
||||||
memoizedLogs
|
|
||||||
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
|
|
||||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
|
||||||
[memoizedLogs],
|
|
||||||
)
|
|
||||||
|
|
||||||
const eventsOnly: CombinedLogEntry[] = useMemo(
|
|
||||||
() =>
|
|
||||||
memoizedEvents
|
|
||||||
.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),
|
|
||||||
[memoizedEvents],
|
|
||||||
)
|
|
||||||
|
|
||||||
const filteredLogsOnly = logsOnly.filter((log) => {
|
|
||||||
const message = log.message || ""
|
|
||||||
const service = log.service || ""
|
|
||||||
const searchTermLower = safeToLowerCase(searchTerm)
|
|
||||||
|
|
||||||
const matchesSearch =
|
|
||||||
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
|
|
||||||
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
|
||||||
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
|
||||||
|
|
||||||
return matchesSearch && matchesLevel && matchesService
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredEventsOnly = eventsOnly.filter((event) => {
|
|
||||||
const message = event.message || ""
|
|
||||||
const service = event.service || ""
|
|
||||||
const searchTermLower = safeToLowerCase(searchTerm)
|
|
||||||
|
|
||||||
const matchesSearch =
|
|
||||||
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
|
|
||||||
const matchesLevel = levelFilter === "all" || event.level === levelFilter
|
|
||||||
const matchesService = serviceFilter === "all" || event.service === serviceFilter
|
|
||||||
|
|
||||||
return matchesSearch && matchesLevel && matchesService
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayedLogsOnly = filteredLogsOnly.slice(0, displayedLogsCount)
|
|
||||||
const displayedEventsOnly = filteredEventsOnly.slice(0, displayedLogsCount)
|
|
||||||
|
|
||||||
const combinedLogs: CombinedLogEntry[] = useMemo(
|
const combinedLogs: CombinedLogEntry[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
[
|
||||||
...memoizedLogs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
|
...logs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
|
||||||
...memoizedEvents.map((event) => ({
|
...events.map((event) => ({
|
||||||
timestamp: event.starttime,
|
timestamp: event.starttime,
|
||||||
level: event.level,
|
level: event.level,
|
||||||
service: event.type,
|
service: event.type,
|
||||||
@@ -481,18 +333,20 @@ export function SystemLogs() {
|
|||||||
sortTimestamp: new Date(event.starttime).getTime(),
|
sortTimestamp: new Date(event.starttime).getTime(),
|
||||||
})),
|
})),
|
||||||
].sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
].sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||||
[memoizedLogs, memoizedEvents],
|
[logs, events],
|
||||||
)
|
)
|
||||||
|
|
||||||
const filteredCombinedLogs = useMemo(
|
const filteredCombinedLogs = useMemo(
|
||||||
() =>
|
() =>
|
||||||
combinedLogs.filter((log) => {
|
combinedLogs.filter((log) => {
|
||||||
const message = log.message || ""
|
|
||||||
const service = log.service || ""
|
|
||||||
const searchTermLower = safeToLowerCase(searchTerm)
|
const searchTermLower = safeToLowerCase(searchTerm)
|
||||||
|
|
||||||
const matchesSearch =
|
const matchesSearch = !searchTermLower ||
|
||||||
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
|
safeToLowerCase(log.message).includes(searchTermLower) ||
|
||||||
|
safeToLowerCase(log.service).includes(searchTermLower) ||
|
||||||
|
safeToLowerCase(log.pid).includes(searchTermLower) ||
|
||||||
|
safeToLowerCase(log.hostname).includes(searchTermLower) ||
|
||||||
|
safeToLowerCase(log.unit).includes(searchTermLower)
|
||||||
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
||||||
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
||||||
|
|
||||||
@@ -501,7 +355,6 @@ export function SystemLogs() {
|
|||||||
[combinedLogs, searchTerm, levelFilter, serviceFilter],
|
[combinedLogs, searchTerm, levelFilter, serviceFilter],
|
||||||
)
|
)
|
||||||
|
|
||||||
// CHANGE: Re-assigning displayedLogs to use the filteredCombinedLogs
|
|
||||||
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
|
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
|
||||||
const hasMoreLogs = displayedLogsCount < filteredCombinedLogs.length
|
const hasMoreLogs = displayedLogsCount < filteredCombinedLogs.length
|
||||||
|
|
||||||
@@ -577,7 +430,6 @@ export function SystemLogs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ADDED: New function for notification source colors
|
|
||||||
const getNotificationSourceColor = (source: string) => {
|
const getNotificationSourceColor = (source: string) => {
|
||||||
if (!source) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
if (!source) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||||
|
|
||||||
@@ -600,7 +452,10 @@ export function SystemLogs() {
|
|||||||
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
|
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueServices = useMemo(() => [...new Set(memoizedLogs.map((log) => log.service))], [memoizedLogs])
|
const uniqueServices = useMemo(
|
||||||
|
() => [...new Set(logs.map((log) => log.service).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
|
||||||
|
[logs],
|
||||||
|
)
|
||||||
|
|
||||||
const getBackupType = (volid: string): "vm" | "lxc" => {
|
const getBackupType = (volid: string): "vm" | "lxc" => {
|
||||||
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
|
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
|
||||||
@@ -770,7 +625,7 @@ export function SystemLogs() {
|
|||||||
<Activity className="h-5 w-5 mr-2" />
|
<Activity className="h-5 w-5 mr-2" />
|
||||||
System Logs & Events
|
System Logs & Events
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button variant="outline" size="sm" onClick={fetchAllData} disabled={loading}>
|
<Button variant="outline" size="sm" onClick={refreshData} disabled={loading}>
|
||||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
|
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
@@ -875,7 +730,6 @@ export function SystemLogs() {
|
|||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search logs & events..."
|
placeholder="Search logs & events..."
|
||||||
// CHANGE: Renamed searchTerm to searchQuery
|
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="pl-10 bg-background border-border"
|
className="pl-10 bg-background border-border"
|
||||||
@@ -928,8 +782,8 @@ export function SystemLogs() {
|
|||||||
<SelectItem key="service-all" value="all">
|
<SelectItem key="service-all" value="all">
|
||||||
All Services
|
All Services
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{uniqueServices.slice(0, 20).map((service, idx) => (
|
{uniqueServices.map((service) => (
|
||||||
<SelectItem key={`service-${service}-${idx}`} value={service}>
|
<SelectItem key={`service-${service}`} value={service}>
|
||||||
{service}
|
{service}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -990,6 +844,7 @@ export function SystemLogs() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
|
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
|
||||||
{log.source}
|
{log.source}
|
||||||
|
{log.unit && log.unit !== log.service && ` • Unit: ${log.unit}`}
|
||||||
{log.pid && ` • PID: ${log.pid}`}
|
{log.pid && ` • PID: ${log.pid}`}
|
||||||
{log.hostname && ` • Host: ${log.hostname}`}
|
{log.hostname && ` • Host: ${log.hostname}`}
|
||||||
</div>
|
</div>
|
||||||
@@ -1009,7 +864,7 @@ export function SystemLogs() {
|
|||||||
<div className="flex justify-center pt-4">
|
<div className="flex justify-center pt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDisplayedLogsCount((prev) => prev + 500)}
|
onClick={() => setDisplayedLogsCount((prev) => prev + 200)}
|
||||||
className="border-border"
|
className="border-border"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
@@ -1057,7 +912,7 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
<ScrollArea className="h-[500px] w-full rounded-md border border-border">
|
<ScrollArea className="h-[500px] w-full rounded-md border border-border">
|
||||||
<div className="space-y-2 p-4">
|
<div className="space-y-2 p-4">
|
||||||
{memoizedBackups.map((backup, index) => {
|
{backups.map((backup, index) => {
|
||||||
const uniqueKey = `backup-${backup.volid.replace(/[/:]/g, "-")}-${backup.timestamp || index}`
|
const uniqueKey = `backup-${backup.volid.replace(/[/:]/g, "-")}-${backup.timestamp || index}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1114,7 +969,7 @@ export function SystemLogs() {
|
|||||||
<TabsContent value="notifications" className="space-y-4">
|
<TabsContent value="notifications" className="space-y-4">
|
||||||
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
|
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
|
||||||
<div className="space-y-2 p-4">
|
<div className="space-y-2 p-4">
|
||||||
{memoizedNotifications.map((notification, index) => {
|
{notifications.map((notification, index) => {
|
||||||
const timestampMs = new Date(notification.timestamp).getTime()
|
const timestampMs = new Date(notification.timestamp).getTime()
|
||||||
const uniqueKey = `notification-${timestampMs}-${notification.service?.substring(0, 10) || "unknown"}-${notification.source?.substring(0, 10) || "unknown"}-${index}`
|
const uniqueKey = `notification-${timestampMs}-${notification.service?.substring(0, 10) || "unknown"}-${notification.source?.substring(0, 10) || "unknown"}-${index}`
|
||||||
|
|
||||||
@@ -1202,6 +1057,12 @@ export function SystemLogs() {
|
|||||||
<div className="text-sm font-medium text-muted-foreground mb-1">Source</div>
|
<div className="text-sm font-medium text-muted-foreground mb-1">Source</div>
|
||||||
<div className="text-sm text-foreground break-all overflow-hidden">{selectedLog.source}</div>
|
<div className="text-sm text-foreground break-all overflow-hidden">{selectedLog.source}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{selectedLog.unit && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-muted-foreground mb-1">Systemd Unit</div>
|
||||||
|
<div className="text-sm text-foreground font-mono break-all overflow-hidden">{selectedLog.unit}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{selectedLog.pid && (
|
{selectedLog.pid && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-1">Process ID</div>
|
<div className="text-sm font-medium text-muted-foreground mb-1">Process ID</div>
|
||||||
|
|||||||
@@ -5153,12 +5153,10 @@ def api_logs():
|
|||||||
if since_days:
|
if since_days:
|
||||||
try:
|
try:
|
||||||
days = int(since_days)
|
days = int(since_days)
|
||||||
cmd = ['journalctl', '--since', f'{days} days ago', '--output', 'json', '--no-pager']
|
# Cap at 90 days to prevent excessive queries
|
||||||
# print(f"[API] Filtering logs since {days} days ago (no limit)")
|
days = min(days, 90)
|
||||||
pass
|
cmd = ['journalctl', '--since', f'{days} days ago', '-n', '10000', '--output', 'json', '--no-pager']
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# print(f"[API] Invalid since_days value: {since_days}")
|
|
||||||
pass
|
|
||||||
cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager']
|
cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager']
|
||||||
else:
|
else:
|
||||||
cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager']
|
cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager']
|
||||||
@@ -5167,11 +5165,11 @@ def api_logs():
|
|||||||
if priority:
|
if priority:
|
||||||
cmd.extend(['-p', priority])
|
cmd.extend(['-p', priority])
|
||||||
|
|
||||||
# Add service filter if specified
|
# Add service filter by SYSLOG_IDENTIFIER (not -u which filters by systemd unit)
|
||||||
if service:
|
# We filter after fetching since journalctl doesn't have a direct SYSLOG_IDENTIFIER flag
|
||||||
cmd.extend(['-u', service])
|
service_filter = service
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
logs = []
|
logs = []
|
||||||
@@ -5191,10 +5189,21 @@ def api_logs():
|
|||||||
priority_num = str(log_entry.get('PRIORITY', '6'))
|
priority_num = str(log_entry.get('PRIORITY', '6'))
|
||||||
level = priority_map.get(priority_num, 'info')
|
level = priority_map.get(priority_num, 'info')
|
||||||
|
|
||||||
|
# Use SYSLOG_IDENTIFIER as primary (matches Proxmox native GUI behavior)
|
||||||
|
# Fall back to _SYSTEMD_UNIT only if SYSLOG_IDENTIFIER is missing
|
||||||
|
syslog_id = log_entry.get('SYSLOG_IDENTIFIER', '')
|
||||||
|
systemd_unit = log_entry.get('_SYSTEMD_UNIT', '')
|
||||||
|
service_name = syslog_id or systemd_unit or 'system'
|
||||||
|
|
||||||
|
# Apply service filter on the resolved service name
|
||||||
|
if service_filter and service_name != service_filter:
|
||||||
|
continue
|
||||||
|
|
||||||
logs.append({
|
logs.append({
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'level': level,
|
'level': level,
|
||||||
'service': log_entry.get('_SYSTEMD_UNIT', log_entry.get('SYSLOG_IDENTIFIER', 'system')),
|
'service': service_name,
|
||||||
|
'unit': systemd_unit,
|
||||||
'message': log_entry.get('MESSAGE', ''),
|
'message': log_entry.get('MESSAGE', ''),
|
||||||
'source': 'journal',
|
'source': 'journal',
|
||||||
'pid': log_entry.get('_PID', ''),
|
'pid': log_entry.get('_PID', ''),
|
||||||
@@ -5210,8 +5219,6 @@ def api_logs():
|
|||||||
'total': 0
|
'total': 0
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# print(f"Error getting logs: {e}")
|
|
||||||
pass
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'error': f'Unable to access system logs: {str(e)}',
|
'error': f'Unable to access system logs: {str(e)}',
|
||||||
'logs': [],
|
'logs': [],
|
||||||
@@ -5230,8 +5237,7 @@ def api_logs_download():
|
|||||||
since_days = request.args.get('since_days', None)
|
since_days = request.args.get('since_days', None)
|
||||||
|
|
||||||
if since_days:
|
if since_days:
|
||||||
days = int(since_days)
|
days = min(int(since_days), 90)
|
||||||
|
|
||||||
cmd = ['journalctl', '--since', f'{days} days ago', '--no-pager']
|
cmd = ['journalctl', '--since', f'{days} days ago', '--no-pager']
|
||||||
else:
|
else:
|
||||||
cmd = ['journalctl', '--since', f'{hours} hours ago', '--no-pager']
|
cmd = ['journalctl', '--since', f'{hours} hours ago', '--no-pager']
|
||||||
@@ -5249,16 +5255,19 @@ def api_logs_download():
|
|||||||
if level != 'all':
|
if level != 'all':
|
||||||
cmd.extend(['-p', level])
|
cmd.extend(['-p', level])
|
||||||
|
|
||||||
# Apply service filter
|
# Apply service filter using SYSLOG_IDENTIFIER grep
|
||||||
|
# Note: We use --grep to match the service name in the log output
|
||||||
|
# since journalctl doesn't have a direct SYSLOG_IDENTIFIER filter flag
|
||||||
if service != 'all':
|
if service != 'all':
|
||||||
cmd.extend(['-u', service])
|
cmd.extend(['--grep', service])
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
import tempfile
|
import tempfile
|
||||||
|
time_desc = f"{since_days} days" if since_days else f"{hours}h"
|
||||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log') as f:
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log') as f:
|
||||||
f.write(f"ProxMenux Log ({log_type}, since {since_days if since_days else f'{hours}h'}) - Generated: {datetime.now().isoformat()}\n")
|
f.write(f"ProxMenux Log ({log_type}, since {time_desc}) - Generated: {datetime.now().isoformat()}\n")
|
||||||
f.write("=" * 80 + "\n\n")
|
f.write("=" * 80 + "\n\n")
|
||||||
f.write(result.stdout)
|
f.write(result.stdout)
|
||||||
temp_path = f.name
|
temp_path = f.name
|
||||||
@@ -5273,8 +5282,6 @@ def api_logs_download():
|
|||||||
return jsonify({'error': 'Failed to generate log file'}), 500
|
return jsonify({'error': 'Failed to generate log file'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# print(f"Error downloading logs: {e}")
|
|
||||||
pass
|
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/notifications', methods=['GET'])
|
@app.route('/api/notifications', methods=['GET'])
|
||||||
@@ -5324,7 +5331,7 @@ def api_notifications():
|
|||||||
notifications.append({
|
notifications.append({
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'type': notif_type,
|
'type': notif_type,
|
||||||
'service': log_entry.get('_SYSTEMD_UNIT', 'proxmox'),
|
'service': log_entry.get('SYSLOG_IDENTIFIER', log_entry.get('_SYSTEMD_UNIT', 'proxmox')),
|
||||||
'message': message,
|
'message': message,
|
||||||
'source': 'journal'
|
'source': 'journal'
|
||||||
})
|
})
|
||||||
@@ -5386,8 +5393,6 @@ def api_notifications():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# print(f"Error getting notifications: {e}")
|
|
||||||
pass
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'error': str(e),
|
'error': str(e),
|
||||||
'notifications': [],
|
'notifications': [],
|
||||||
@@ -5431,7 +5436,7 @@ def api_notifications_download():
|
|||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
import tempfile
|
import tempfile
|
||||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log') as f:
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log') as f:
|
||||||
f.write(f"ProxMenux Log ({log_type}, since {since_days if since_days else f'{hours}h'}) - Generated: {datetime.now().isoformat()}\n")
|
f.write(f"ProxMenux Notification Log (around {timestamp}) - Generated: {datetime.now().isoformat()}\n")
|
||||||
f.write("=" * 80 + "\n\n")
|
f.write("=" * 80 + "\n\n")
|
||||||
f.write(result.stdout)
|
f.write(result.stdout)
|
||||||
temp_path = f.name
|
temp_path = f.name
|
||||||
@@ -5446,8 +5451,6 @@ def api_notifications_download():
|
|||||||
return jsonify({'error': 'Failed to generate log file'}), 500
|
return jsonify({'error': 'Failed to generate log file'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# print(f"Error downloading logs: {e}")
|
|
||||||
pass
|
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/backups', methods=['GET'])
|
@app.route('/api/backups', methods=['GET'])
|
||||||
|
|||||||
Reference in New Issue
Block a user