mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-18 11:36:17 +00:00
Update AppImage
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Terminal,
|
Terminal,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
|
Clock,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ export function SystemLogs() {
|
|||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
const [levelFilter, setLevelFilter] = useState("all")
|
const [levelFilter, setLevelFilter] = useState("all")
|
||||||
const [serviceFilter, setServiceFilter] = useState("all")
|
const [serviceFilter, setServiceFilter] = useState("all")
|
||||||
|
const [timeRange, setTimeRange] = useState("1") // Default 1 minute for fast loading
|
||||||
const [activeTab, setActiveTab] = useState("logs")
|
const [activeTab, setActiveTab] = useState("logs")
|
||||||
|
|
||||||
const [selectedLog, setSelectedLog] = useState<SystemLog | null>(null)
|
const [selectedLog, setSelectedLog] = useState<SystemLog | null>(null)
|
||||||
@@ -115,7 +117,7 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAllData()
|
fetchAllData()
|
||||||
}, [])
|
}, [timeRange]) // Re-fetch when time range changes
|
||||||
|
|
||||||
const fetchAllData = async () => {
|
const fetchAllData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -155,7 +157,7 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
||||||
try {
|
try {
|
||||||
const apiUrl = getApiUrl("/api/logs")
|
const apiUrl = getApiUrl(`/api/logs?minutes=${timeRange}`)
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -294,6 +296,20 @@ export function SystemLogs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNotificationTypeColor = (type: string) => {
|
||||||
|
const lowerType = type.toLowerCase()
|
||||||
|
if (lowerType.includes("error") || lowerType.includes("critical") || lowerType.includes("alert")) {
|
||||||
|
return "bg-red-500/10 text-red-500 border-red-500/20"
|
||||||
|
} else if (lowerType.includes("warning") || lowerType.includes("warn")) {
|
||||||
|
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||||
|
} else if (lowerType.includes("success") || lowerType.includes("ok")) {
|
||||||
|
return "bg-green-500/10 text-green-500 border-green-500/20"
|
||||||
|
} else if (lowerType.includes("info")) {
|
||||||
|
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
|
||||||
|
}
|
||||||
|
return "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||||
|
}
|
||||||
|
|
||||||
const logCounts = {
|
const logCounts = {
|
||||||
total: logs.length,
|
total: logs.length,
|
||||||
error: logs.filter((log) => ["error", "critical", "emergency", "alert"].includes(log.level)).length,
|
error: logs.filter((log) => ["error", "critical", "emergency", "alert"].includes(log.level)).length,
|
||||||
@@ -552,6 +568,22 @@ export function SystemLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
|
<SelectTrigger className="w-full sm:w-[180px] bg-background border-border">
|
||||||
|
<Clock className="h-4 w-4 mr-2" />
|
||||||
|
<SelectValue placeholder="Time range" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1">Last 1 minute</SelectItem>
|
||||||
|
<SelectItem value="5">Last 5 minutes</SelectItem>
|
||||||
|
<SelectItem value="15">Last 15 minutes</SelectItem>
|
||||||
|
<SelectItem value="30">Last 30 minutes</SelectItem>
|
||||||
|
<SelectItem value="60">Last 1 hour</SelectItem>
|
||||||
|
<SelectItem value="360">Last 6 hours</SelectItem>
|
||||||
|
<SelectItem value="1440">Last 24 hours</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<Select value={levelFilter} onValueChange={setLevelFilter}>
|
<Select value={levelFilter} onValueChange={setLevelFilter}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px] bg-background border-border">
|
<SelectTrigger className="w-full sm:w-[180px] bg-background border-border">
|
||||||
<SelectValue placeholder="Filter by level" />
|
<SelectValue placeholder="Filter by level" />
|
||||||
@@ -593,7 +625,7 @@ export function SystemLogs() {
|
|||||||
{filteredLogs.map((log, index) => (
|
{filteredLogs.map((log, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-start space-x-4 p-3 rounded-lg bg-card/50 border border-border/50 hover:bg-card/80 transition-colors cursor-pointer"
|
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg bg-card/50 border border-border/50 hover:bg-card/80 transition-colors cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedLog(log)
|
setSelectedLog(log)
|
||||||
setIsLogModalOpen(true)
|
setIsLogModalOpen(true)
|
||||||
@@ -640,7 +672,7 @@ export function SystemLogs() {
|
|||||||
{events.map((event, index) => (
|
{events.map((event, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-start space-x-4 p-3 rounded-lg bg-card/50 border border-border/50 hover:bg-card/80 transition-colors cursor-pointer"
|
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg bg-card/50 border border-border/50 hover:bg-card/80 transition-colors cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEvent(event)
|
setSelectedEvent(event)
|
||||||
setIsEventModalOpen(true)
|
setIsEventModalOpen(true)
|
||||||
@@ -760,27 +792,28 @@ export function SystemLogs() {
|
|||||||
{notifications.map((notification, index) => (
|
{notifications.map((notification, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-start space-x-4 p-3 rounded-lg bg-card/50 border border-border/50 hover:bg-card/80 transition-colors cursor-pointer"
|
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg bg-card/50 border border-border/50 hover:bg-card/80 transition-colors cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedNotification(notification)
|
setSelectedNotification(notification)
|
||||||
setIsNotificationModalOpen(true)
|
setIsNotificationModalOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">{getNotificationIcon(notification.type)}</div>
|
<div className="flex-shrink-0">
|
||||||
|
<Badge variant="outline" className={getNotificationTypeColor(notification.type)}>
|
||||||
|
{getNotificationIcon(notification.type)}
|
||||||
|
{notification.type.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<div className="text-sm font-medium text-muted-foreground capitalize truncate">
|
<div className="text-sm font-medium text-foreground truncate">{notification.service}</div>
|
||||||
{notification.type}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground font-mono whitespace-nowrap ml-2">
|
<div className="text-xs text-muted-foreground font-mono whitespace-nowrap ml-2">
|
||||||
{notification.timestamp}
|
{notification.timestamp}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-foreground mb-1 line-clamp-2">{notification.message}</div>
|
<div className="text-sm text-foreground mb-1 line-clamp-2">{notification.message}</div>
|
||||||
<div className="text-xs text-muted-foreground truncate">
|
<div className="text-xs text-muted-foreground truncate">Source: {notification.source}</div>
|
||||||
Service: {notification.service} • Source: {notification.source}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -988,10 +1021,10 @@ export function SystemLogs() {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-1">Type</div>
|
<div className="text-sm font-medium text-muted-foreground mb-1">Type</div>
|
||||||
<div className="flex items-center gap-2">
|
<Badge variant="outline" className={getNotificationTypeColor(selectedNotification.type)}>
|
||||||
{getNotificationIcon(selectedNotification.type)}
|
{getNotificationIcon(selectedNotification.type)}
|
||||||
<span className="text-sm text-foreground capitalize">{selectedNotification.type}</span>
|
{selectedNotification.type.toUpperCase()}
|
||||||
</div>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-1">Timestamp</div>
|
<div className="text-sm font-medium text-muted-foreground mb-1">Timestamp</div>
|
||||||
@@ -1007,10 +1040,11 @@ export function SystemLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-2">Message</div>
|
<div className="text-sm font-medium text-muted-foreground mb-2">Message (First 20 lines)</div>
|
||||||
<div className="p-4 rounded-lg bg-muted/50 border border-border">
|
<div className="p-4 rounded-lg bg-muted/50 border border-border max-h-[300px] overflow-y-auto">
|
||||||
<pre className="text-sm text-foreground whitespace-pre-wrap break-words">
|
<pre className="text-sm text-foreground whitespace-pre-wrap break-words">
|
||||||
{selectedNotification.message}
|
{selectedNotification.message.split("\n").slice(0, 20).join("\n")}
|
||||||
|
{selectedNotification.message.split("\n").length > 20 && "\n\n... (download full log to see more)"}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3339,11 +3339,11 @@ def api_vms():
|
|||||||
def api_logs():
|
def api_logs():
|
||||||
"""Get system logs"""
|
"""Get system logs"""
|
||||||
try:
|
try:
|
||||||
limit = request.args.get('limit', '200')
|
minutes = request.args.get('minutes', '1') # Default 1 minute for fast loading
|
||||||
priority = request.args.get('priority', None) # 0-7 (0=emerg, 3=err, 4=warning, 6=info)
|
priority = request.args.get('priority', None) # 0-7 (0=emerg, 3=err, 4=warning, 6=info)
|
||||||
service = request.args.get('service', None)
|
service = request.args.get('service', None)
|
||||||
|
|
||||||
cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager']
|
cmd = ['journalctl', '--since', f'{minutes} minutes ago', '--output', 'json', '--no-pager']
|
||||||
|
|
||||||
# Add priority filter if specified
|
# Add priority filter if specified
|
||||||
if priority:
|
if priority:
|
||||||
@@ -3569,24 +3569,25 @@ def api_notifications_download():
|
|||||||
if not timestamp:
|
if not timestamp:
|
||||||
return jsonify({'error': 'Timestamp parameter required'}), 400
|
return jsonify({'error': 'Timestamp parameter required'}), 400
|
||||||
|
|
||||||
# Parse the timestamp and calculate time range
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Parse timestamp format: "2025-10-11 14:27:35"
|
# Parse timestamp format: "2025-10-11 14:27:35"
|
||||||
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
|
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
|
||||||
since_time = (dt - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S")
|
# Use a very small time window (2 minutes) to get just this notification
|
||||||
until_time = (dt + timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S")
|
since_time = (dt - timedelta(minutes=1)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
until_time = (dt + timedelta(minutes=1)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# If parsing fails, use a default range
|
# If parsing fails, use a default range
|
||||||
since_time = "1 hour ago"
|
since_time = "2 minutes ago"
|
||||||
until_time = "now"
|
until_time = "now"
|
||||||
|
|
||||||
|
# Get logs around the specific timestamp
|
||||||
cmd = [
|
cmd = [
|
||||||
'journalctl',
|
'journalctl',
|
||||||
'--since', since_time,
|
'--since', since_time,
|
||||||
'--until', until_time,
|
'--until', until_time,
|
||||||
'-n', '1000',
|
'-n', '50', # Limit to 50 lines around the notification
|
||||||
'--no-pager'
|
'--no-pager'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3596,7 +3597,7 @@ def api_notifications_download():
|
|||||||
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"Notification Log - {timestamp}\n")
|
f.write(f"Notification Log - {timestamp}\n")
|
||||||
f.write(f"Time Range: {since_time} to {until_time}\n")
|
f.write(f"Time Window: {since_time} to {until_time}\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
|
||||||
|
|||||||
Reference in New Issue
Block a user