mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update logs
This commit is contained in:
@@ -9,7 +9,8 @@ import {
|
|||||||
Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Key, Copy, Eye, EyeOff,
|
Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Key, Copy, Eye, EyeOff,
|
||||||
Trash2, RefreshCw, Clock, ShieldCheck, Globe, FileKey, AlertTriangle,
|
Trash2, RefreshCw, Clock, ShieldCheck, Globe, FileKey, AlertTriangle,
|
||||||
Flame, Bug, Search, Download, Power, PowerOff, Plus, Minus, Activity, Settings, Ban,
|
Flame, Bug, Search, Download, Power, PowerOff, Plus, Minus, Activity, Settings, Ban,
|
||||||
FileText, Printer, Play, BarChart3, TriangleAlert, ChevronDown,
|
FileText, Printer, Play, BarChart3, TriangleAlert, ChevronDown, ArrowDownLeft, ArrowUpRight,
|
||||||
|
ChevronRight, Network, Zap, Pencil, Check, X,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { getApiUrl, fetchApi } from "../lib/api-config"
|
import { getApiUrl, fetchApi } from "../lib/api-config"
|
||||||
import { TwoFactorSetup } from "./two-factor-setup"
|
import { TwoFactorSetup } from "./two-factor-setup"
|
||||||
@@ -88,6 +89,14 @@ export function Security() {
|
|||||||
})
|
})
|
||||||
const [addingRule, setAddingRule] = useState(false)
|
const [addingRule, setAddingRule] = useState(false)
|
||||||
const [deletingRuleIdx, setDeletingRuleIdx] = useState<number | null>(null)
|
const [deletingRuleIdx, setDeletingRuleIdx] = useState<number | null>(null)
|
||||||
|
const [expandedRuleKey, setExpandedRuleKey] = useState<string | null>(null)
|
||||||
|
const [editingRuleKey, setEditingRuleKey] = useState<string | null>(null)
|
||||||
|
const [editRule, setEditRule] = useState({
|
||||||
|
direction: "IN", action: "ACCEPT", protocol: "tcp",
|
||||||
|
dport: "", sport: "", source: "", iface: "", comment: "", level: "host",
|
||||||
|
})
|
||||||
|
const [savingRule, setSavingRule] = useState(false)
|
||||||
|
const [networkInterfaces, setNetworkInterfaces] = useState<{name: string, type: string, status: string}[]>([])
|
||||||
|
|
||||||
// Security Tools state
|
// Security Tools state
|
||||||
const [toolsLoading, setToolsLoading] = useState(true)
|
const [toolsLoading, setToolsLoading] = useState(true)
|
||||||
@@ -174,6 +183,7 @@ export function Security() {
|
|||||||
const [proxmoxCertInfo, setProxmoxCertInfo] = useState<{subject?: string; expires?: string; issuer?: string; is_self_signed?: boolean} | null>(null)
|
const [proxmoxCertInfo, setProxmoxCertInfo] = useState<{subject?: string; expires?: string; issuer?: string; is_self_signed?: boolean} | null>(null)
|
||||||
const [loadingSsl, setLoadingSsl] = useState(true)
|
const [loadingSsl, setLoadingSsl] = useState(true)
|
||||||
const [configuringSsl, setConfiguringSsl] = useState(false)
|
const [configuringSsl, setConfiguringSsl] = useState(false)
|
||||||
|
const [sslRestarting, setSslRestarting] = useState(false)
|
||||||
const [showCustomCertForm, setShowCustomCertForm] = useState(false)
|
const [showCustomCertForm, setShowCustomCertForm] = useState(false)
|
||||||
const [customCertPath, setCustomCertPath] = useState("")
|
const [customCertPath, setCustomCertPath] = useState("")
|
||||||
const [customKeyPath, setCustomKeyPath] = useState("")
|
const [customKeyPath, setCustomKeyPath] = useState("")
|
||||||
@@ -183,6 +193,7 @@ export function Security() {
|
|||||||
loadApiTokens()
|
loadApiTokens()
|
||||||
loadSslStatus()
|
loadSslStatus()
|
||||||
loadFirewallStatus()
|
loadFirewallStatus()
|
||||||
|
loadNetworkInterfaces()
|
||||||
loadSecurityTools()
|
loadSecurityTools()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -208,6 +219,22 @@ export function Security() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadNetworkInterfaces = async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchApi("/api/network")
|
||||||
|
// The API returns interfaces in separate arrays: physical_interfaces, bridge_interfaces, etc.
|
||||||
|
// The generic "interfaces" array only holds uncategorized types and is usually empty.
|
||||||
|
const all = [
|
||||||
|
...(data.physical_interfaces || []),
|
||||||
|
...(data.bridge_interfaces || []),
|
||||||
|
...(data.interfaces || []),
|
||||||
|
].sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||||
|
setNetworkInterfaces(all)
|
||||||
|
} catch {
|
||||||
|
// Silently fail - select will just show "Any interface"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadSecurityTools = async () => {
|
const loadSecurityTools = async () => {
|
||||||
try {
|
try {
|
||||||
setToolsLoading(true)
|
setToolsLoading(true)
|
||||||
@@ -470,6 +497,51 @@ export function Security() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startEditRule = (rule: any) => {
|
||||||
|
const ruleKey = `${rule.source_file}-${rule.rule_index}`
|
||||||
|
const comment = rule.raw?.includes("#") ? rule.raw.split("#").slice(1).join("#").trim() : ""
|
||||||
|
setEditingRuleKey(ruleKey)
|
||||||
|
setEditRule({
|
||||||
|
direction: rule.direction || "IN",
|
||||||
|
action: rule.action || "ACCEPT",
|
||||||
|
protocol: rule.p || "tcp",
|
||||||
|
dport: rule.dport || "",
|
||||||
|
sport: "",
|
||||||
|
source: rule.source || "",
|
||||||
|
iface: rule.i || "",
|
||||||
|
comment,
|
||||||
|
level: rule.source_file || "host",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveEditRule = async (oldRuleIndex: number, oldLevel: string) => {
|
||||||
|
setSavingRule(true)
|
||||||
|
setError("")
|
||||||
|
setSuccess("")
|
||||||
|
try {
|
||||||
|
const data = await fetchApi("/api/security/firewall/rules/edit", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
rule_index: oldRuleIndex,
|
||||||
|
level: oldLevel,
|
||||||
|
new_rule: editRule,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (data.success) {
|
||||||
|
setSuccess(data.message || "Rule updated successfully")
|
||||||
|
setEditingRuleKey(null)
|
||||||
|
loadFirewallStatus()
|
||||||
|
} else {
|
||||||
|
setError(data.message || "Failed to update rule")
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to update rule")
|
||||||
|
} finally {
|
||||||
|
setSavingRule(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleFirewallToggle = async (level: "host" | "cluster", enable: boolean) => {
|
const handleFirewallToggle = async (level: "host" | "cluster", enable: boolean) => {
|
||||||
setFirewallAction(true)
|
setFirewallAction(true)
|
||||||
setError("")
|
setError("")
|
||||||
@@ -1256,13 +1328,49 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for the monitor service to come back on the new protocol, then redirect
|
||||||
|
const waitForServiceAndRedirect = async (newProtocol: "https" | "http") => {
|
||||||
|
const host = window.location.hostname
|
||||||
|
const port = window.location.port || "8008"
|
||||||
|
const newUrl = `${newProtocol}://${host}:${port}${window.location.pathname}`
|
||||||
|
|
||||||
|
// Wait for service to restart (try up to 30 seconds)
|
||||||
|
const maxAttempts = 15
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
await new Promise(r => setTimeout(r, 2000))
|
||||||
|
try {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 3000)
|
||||||
|
const resp = await fetch(`${newProtocol}://${host}:${port}/api/ssl/status`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
// For self-signed certs, we need to handle rejection
|
||||||
|
mode: "no-cors"
|
||||||
|
}).catch(() => null)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
// For HTTPS with self-signed certs, even a failed CORS request means the server is up
|
||||||
|
if (resp || newProtocol === "https") {
|
||||||
|
// Give it one more second to fully stabilize
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
|
window.location.href = newUrl
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Server not ready yet, keep waiting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: redirect anyway after timeout
|
||||||
|
window.location.href = newUrl
|
||||||
|
}
|
||||||
|
|
||||||
const handleEnableSsl = async (source: "proxmox" | "custom", certPath?: string, keyPath?: string) => {
|
const handleEnableSsl = async (source: "proxmox" | "custom", certPath?: string, keyPath?: string) => {
|
||||||
setConfiguringSsl(true)
|
setConfiguringSsl(true)
|
||||||
setError("")
|
setError("")
|
||||||
setSuccess("")
|
setSuccess("")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body: Record<string, string> = { source }
|
const body: Record<string, string | boolean> = { source, auto_restart: true }
|
||||||
if (source === "custom" && certPath && keyPath) {
|
if (source === "custom" && certPath && keyPath) {
|
||||||
body.cert_path = certPath
|
body.cert_path = certPath
|
||||||
body.key_path = keyPath
|
body.key_path = keyPath
|
||||||
@@ -1275,25 +1383,27 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setSuccess(data.message || "SSL configured successfully. Restart the monitor service to apply.")
|
|
||||||
setSslEnabled(true)
|
setSslEnabled(true)
|
||||||
setSslSource(source)
|
setSslSource(source)
|
||||||
setShowCustomCertForm(false)
|
setShowCustomCertForm(false)
|
||||||
setCustomCertPath("")
|
setCustomCertPath("")
|
||||||
setCustomKeyPath("")
|
setCustomKeyPath("")
|
||||||
loadSslStatus()
|
setConfiguringSsl(false)
|
||||||
|
setSslRestarting(true)
|
||||||
|
setSuccess("SSL enabled. Restarting service and switching to HTTPS...")
|
||||||
|
await waitForServiceAndRedirect("https")
|
||||||
} else {
|
} else {
|
||||||
setError(data.message || "Failed to configure SSL")
|
setError(data.message || "Failed to configure SSL")
|
||||||
|
setConfiguringSsl(false)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to configure SSL")
|
setError(err instanceof Error ? err.message : "Failed to configure SSL")
|
||||||
} finally {
|
|
||||||
setConfiguringSsl(false)
|
setConfiguringSsl(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDisableSsl = async () => {
|
const handleDisableSsl = async () => {
|
||||||
if (!confirm("Are you sure you want to disable HTTPS? The monitor will revert to HTTP after restart.")) {
|
if (!confirm("Are you sure you want to disable HTTPS? The monitor will switch to HTTP.")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1302,21 +1412,27 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
setSuccess("")
|
setSuccess("")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchApi("/api/ssl/disable", { method: "POST" })
|
const data = await fetchApi("/api/ssl/disable", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ auto_restart: true }),
|
||||||
|
})
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setSuccess(data.message || "SSL disabled. Restart the monitor service to apply.")
|
|
||||||
setSslEnabled(false)
|
setSslEnabled(false)
|
||||||
setSslSource("none")
|
setSslSource("none")
|
||||||
setSslCertPath("")
|
setSslCertPath("")
|
||||||
setSslKeyPath("")
|
setSslKeyPath("")
|
||||||
loadSslStatus()
|
setConfiguringSsl(false)
|
||||||
|
setSslRestarting(true)
|
||||||
|
setSuccess("SSL disabled. Restarting service and switching to HTTP...")
|
||||||
|
await waitForServiceAndRedirect("http")
|
||||||
} else {
|
} else {
|
||||||
setError(data.message || "Failed to disable SSL")
|
setError(data.message || "Failed to disable SSL")
|
||||||
|
setConfiguringSsl(false)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to disable SSL")
|
setError(err instanceof Error ? err.message : "Failed to disable SSL")
|
||||||
} finally {
|
|
||||||
setConfiguringSsl(false)
|
setConfiguringSsl(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1683,10 +1799,10 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
onClick={handleDisableSsl}
|
onClick={handleDisableSsl}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={configuringSsl}
|
disabled={configuringSsl || sslRestarting}
|
||||||
className="mt-2 text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent"
|
className="mt-2 text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent"
|
||||||
>
|
>
|
||||||
{configuringSsl ? "Disabling..." : "Disable HTTPS"}
|
{configuringSsl ? "Disabling..." : sslRestarting ? "Restarting..." : "Disable HTTPS"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1722,7 +1838,7 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => handleEnableSsl("proxmox")}
|
onClick={() => handleEnableSsl("proxmox")}
|
||||||
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
||||||
disabled={configuringSsl}
|
disabled={configuringSsl || sslRestarting}
|
||||||
>
|
>
|
||||||
{configuringSsl ? (
|
{configuringSsl ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1793,9 +1909,9 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleEnableSsl("custom", customCertPath, customKeyPath)}
|
onClick={() => handleEnableSsl("custom", customCertPath, customKeyPath)}
|
||||||
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
||||||
disabled={configuringSsl || !customCertPath || !customKeyPath}
|
disabled={configuringSsl || sslRestarting || !customCertPath || !customKeyPath}
|
||||||
>
|
>
|
||||||
{configuringSsl ? "Configuring..." : "Enable HTTPS"}
|
{configuringSsl ? "Configuring..." : "Enable HTTPS"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1817,14 +1933,27 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Info note about restart */}
|
{/* Restarting overlay or info note */}
|
||||||
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
|
{sslRestarting ? (
|
||||||
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
|
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4 flex items-center gap-3">
|
||||||
<p className="text-sm text-blue-500">
|
<div className="h-5 w-5 border-2 border-amber-500 border-t-transparent rounded-full animate-spin flex-shrink-0" />
|
||||||
Changes to SSL configuration require a monitor service restart to take effect.
|
<div>
|
||||||
The service will automatically use HTTPS on port 8008 when enabled.
|
<p className="text-sm font-medium text-amber-500">
|
||||||
</p>
|
Restarting monitor service...
|
||||||
</div>
|
</p>
|
||||||
|
<p className="text-xs text-amber-400 mt-0.5">
|
||||||
|
The page will automatically redirect to the new address.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-blue-500">
|
||||||
|
SSL changes will automatically restart the monitor service and redirect to the new address.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -2283,6 +2412,90 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Rules Summary Dashboard */}
|
||||||
|
{firewallData.rules.length > 0 && (() => {
|
||||||
|
const acceptCount = firewallData.rules.filter(r => r.action === "ACCEPT").length
|
||||||
|
const dropCount = firewallData.rules.filter(r => r.action === "DROP").length
|
||||||
|
const rejectCount = firewallData.rules.filter(r => r.action === "REJECT").length
|
||||||
|
const blockCount = dropCount + rejectCount
|
||||||
|
const total = firewallData.rules.length
|
||||||
|
const clusterCount = firewallData.rules.filter(r => r.source_file === "cluster").length
|
||||||
|
const hostCount = firewallData.rules.filter(r => r.source_file === "host").length
|
||||||
|
const inCount = firewallData.rules.filter(r => (r.direction || "IN") === "IN").length
|
||||||
|
const outCount = firewallData.rules.filter(r => r.direction === "OUT").length
|
||||||
|
// Collect unique protected ports
|
||||||
|
const protectedPorts = new Set<string>()
|
||||||
|
firewallData.rules.forEach(r => {
|
||||||
|
if (r.dport) r.dport.split(",").forEach(p => protectedPorts.add(p.trim()))
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground">Rules Overview</h3>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
|
<div className="p-3 bg-muted/50 rounded-lg border border-border text-center">
|
||||||
|
<p className="text-lg font-bold text-foreground">{total}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Total Rules</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-green-500/5 rounded-lg border border-green-500/20 text-center">
|
||||||
|
<p className="text-lg font-bold text-green-500">{acceptCount}</p>
|
||||||
|
<p className="text-[10px] text-green-500/70 uppercase tracking-wider">Accept</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-red-500/5 rounded-lg border border-red-500/20 text-center">
|
||||||
|
<p className="text-lg font-bold text-red-500">{blockCount}</p>
|
||||||
|
<p className="text-[10px] text-red-500/70 uppercase tracking-wider">Block / Reject</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-muted/50 rounded-lg border border-border text-center">
|
||||||
|
<p className="text-lg font-bold text-foreground">{protectedPorts.size}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Ports Covered</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Visual bar */}
|
||||||
|
<div className="space-y-1.5 sm:space-y-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-2 rounded-full bg-muted overflow-hidden flex">
|
||||||
|
{acceptCount > 0 && (
|
||||||
|
<div className="h-full bg-green-500 transition-all" style={{ width: `${(acceptCount / total) * 100}%` }} />
|
||||||
|
)}
|
||||||
|
{dropCount > 0 && (
|
||||||
|
<div className="h-full bg-red-500 transition-all" style={{ width: `${(dropCount / total) * 100}%` }} />
|
||||||
|
)}
|
||||||
|
{rejectCount > 0 && (
|
||||||
|
<div className="h-full bg-orange-500 transition-all" style={{ width: `${(rejectCount / total) * 100}%` }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:flex items-center gap-3 text-[10px] text-muted-foreground flex-shrink-0">
|
||||||
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-green-500" />Accept</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-red-500" />Drop</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-orange-500" />Reject</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex sm:hidden items-center gap-3 text-[10px] text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-green-500" />Accept</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-red-500" />Drop</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-orange-500" />Reject</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Scope breakdown */}
|
||||||
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Globe className="h-3 w-3 text-blue-400" /> Cluster: {clusterCount}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Shield className="h-3 w-3 text-purple-400" /> Host: {hostCount}
|
||||||
|
</span>
|
||||||
|
<span className="text-border">|</span>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<ArrowDownLeft className="h-3 w-3" /> IN: {inCount}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<ArrowUpRight className="h-3 w-3" /> OUT: {outCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Firewall Rules */}
|
{/* Firewall Rules */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -2308,6 +2521,42 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
<p className="text-sm font-semibold text-orange-500">New Firewall Rule</p>
|
<p className="text-sm font-semibold text-orange-500">New Firewall Rule</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Service Presets */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Quick Presets</p>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{[
|
||||||
|
{ label: "HTTP", port: "80", proto: "tcp", comment: "HTTP Web" },
|
||||||
|
{ label: "HTTPS", port: "443", proto: "tcp", comment: "HTTPS Web" },
|
||||||
|
{ label: "SSH", port: "22", proto: "tcp", comment: "SSH Remote Access" },
|
||||||
|
{ label: "DNS", port: "53", proto: "udp", comment: "DNS" },
|
||||||
|
{ label: "SMTP", port: "25", proto: "tcp", comment: "SMTP Mail" },
|
||||||
|
{ label: "NFS", port: "2049", proto: "tcp", comment: "NFS" },
|
||||||
|
{ label: "SMB", port: "445", proto: "tcp", comment: "SMB/CIFS" },
|
||||||
|
{ label: "Ping", port: "", proto: "icmp", comment: "ICMP Ping" },
|
||||||
|
].map((preset) => (
|
||||||
|
<Button
|
||||||
|
key={preset.label}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setNewRule({
|
||||||
|
...newRule,
|
||||||
|
dport: preset.port,
|
||||||
|
protocol: preset.proto,
|
||||||
|
comment: preset.comment,
|
||||||
|
direction: "IN",
|
||||||
|
action: "ACCEPT",
|
||||||
|
})}
|
||||||
|
className="h-6 text-[10px] px-2 text-muted-foreground border-border hover:text-orange-500 hover:border-orange-500/30 bg-transparent"
|
||||||
|
>
|
||||||
|
<Zap className="h-2.5 w-2.5 mr-1" />
|
||||||
|
{preset.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs text-muted-foreground">Direction</Label>
|
<Label className="text-xs text-muted-foreground">Direction</Label>
|
||||||
@@ -2372,12 +2621,18 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs text-muted-foreground">Interface (optional)</Label>
|
<Label className="text-xs text-muted-foreground">Interface (optional)</Label>
|
||||||
<Input
|
<select
|
||||||
placeholder="e.g. vmbr0"
|
|
||||||
value={newRule.iface}
|
value={newRule.iface}
|
||||||
onChange={(e) => setNewRule({...newRule, iface: e.target.value})}
|
onChange={(e) => setNewRule({...newRule, iface: e.target.value})}
|
||||||
className="h-9 text-sm"
|
className="w-full h-9 rounded-md border border-border bg-card px-3 text-sm"
|
||||||
/>
|
>
|
||||||
|
<option value="">Any interface</option>
|
||||||
|
{networkInterfaces.map((iface) => (
|
||||||
|
<option key={iface.name} value={iface.name}>
|
||||||
|
{iface.name} ({iface.type}{iface.status === "up" ? ", up" : ", down"})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs text-muted-foreground">Apply to</Label>
|
<Label className="text-xs text-muted-foreground">Apply to</Label>
|
||||||
@@ -2432,51 +2687,242 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
{firewallData.rules.length > 0 ? (
|
{firewallData.rules.length > 0 ? (
|
||||||
<div className="border border-border rounded-lg overflow-hidden">
|
<div className="border border-border rounded-lg overflow-hidden">
|
||||||
{/* Table header */}
|
{/* Table header */}
|
||||||
<div className="grid grid-cols-[auto_1fr_auto_auto_auto_auto_auto] gap-2 p-2.5 bg-muted/50 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
<div className="hidden sm:grid grid-cols-[2rem_4.5rem_2rem_3rem_5rem_1fr_3.5rem_2rem] gap-2 px-3 py-2 bg-muted/50 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider items-center">
|
||||||
<span className="w-14">Action</span>
|
<span />
|
||||||
<span>Direction</span>
|
<span>Action</span>
|
||||||
<span className="w-12">Proto</span>
|
<span />
|
||||||
<span className="w-20">Port</span>
|
<span>Proto</span>
|
||||||
<span className="w-28 hidden sm:block">Source</span>
|
<span>Port</span>
|
||||||
<span className="w-14">Level</span>
|
<span>Source</span>
|
||||||
<span className="w-8" />
|
<span>Level</span>
|
||||||
|
<span />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-border max-h-64 overflow-y-auto">
|
<div className="divide-y divide-border max-h-80 overflow-y-auto">
|
||||||
{firewallData.rules.map((rule, idx) => (
|
{firewallData.rules.map((rule, idx) => {
|
||||||
<div key={idx} className="grid grid-cols-[auto_1fr_auto_auto_auto_auto_auto] gap-2 p-2.5 items-center hover:bg-muted/20 transition-colors">
|
const ruleKey = `${rule.source_file}-${rule.rule_index}`
|
||||||
<span className={`w-14 px-1.5 py-0.5 rounded text-[10px] font-bold text-center ${
|
const isExpanded = expandedRuleKey === ruleKey
|
||||||
rule.action === "ACCEPT" ? "bg-green-500/10 text-green-500" :
|
const direction = rule.direction || "IN"
|
||||||
rule.action === "DROP" ? "bg-red-500/10 text-red-500" :
|
const comment = rule.raw?.includes("#") ? rule.raw.split("#").slice(1).join("#").trim() : ""
|
||||||
rule.action === "REJECT" ? "bg-orange-500/10 text-orange-500" :
|
|
||||||
"bg-gray-500/10 text-gray-500"
|
return (
|
||||||
}`}>
|
<div key={ruleKey}>
|
||||||
{rule.action || "?"}
|
{/* Main row */}
|
||||||
</span>
|
<div
|
||||||
<span className="text-xs text-muted-foreground font-mono">{rule.direction || "IN"}</span>
|
className="grid grid-cols-[2rem_4.5rem_1fr_2rem] sm:grid-cols-[2rem_4.5rem_2rem_3rem_5rem_1fr_3.5rem_2rem] gap-2 px-3 py-2.5 items-center hover:bg-white/5 transition-colors cursor-pointer"
|
||||||
<span className="w-12 text-xs text-blue-400 font-mono">{rule.p || "-"}</span>
|
onClick={() => setExpandedRuleKey(isExpanded ? null : ruleKey)}
|
||||||
<span className="w-20 text-xs text-foreground font-mono">{rule.dport || "-"}</span>
|
>
|
||||||
<span className="w-28 text-xs text-muted-foreground font-mono hidden sm:block truncate">{rule.source || "any"}</span>
|
{/* Direction icon */}
|
||||||
<span className={`w-14 text-[10px] px-1.5 py-0.5 rounded text-center ${
|
<div className="flex items-center justify-center">
|
||||||
rule.source_file === "cluster" ? "bg-blue-500/10 text-blue-400" : "bg-purple-500/10 text-purple-400"
|
{direction === "IN" ? (
|
||||||
}`}>
|
<ArrowDownLeft className="h-4 w-4 text-blue-400" />
|
||||||
{rule.source_file}
|
) : (
|
||||||
</span>
|
<ArrowUpRight className="h-4 w-4 text-amber-400" />
|
||||||
<Button
|
)}
|
||||||
variant="ghost"
|
</div>
|
||||||
size="sm"
|
{/* Action badge */}
|
||||||
onClick={() => handleDeleteRule(rule.rule_index, rule.source_file)}
|
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold text-center ${
|
||||||
disabled={deletingRuleIdx === rule.rule_index}
|
rule.action === "ACCEPT" ? "bg-green-500/10 text-green-500" :
|
||||||
className="w-8 h-7 p-0 text-red-500/50 hover:text-red-500 hover:bg-red-500/10"
|
rule.action === "DROP" ? "bg-red-500/10 text-red-500" :
|
||||||
>
|
rule.action === "REJECT" ? "bg-orange-500/10 text-orange-500" :
|
||||||
{deletingRuleIdx === rule.rule_index ? (
|
"bg-gray-500/10 text-gray-500"
|
||||||
<div className="animate-spin h-3 w-3 border-2 border-red-500 border-t-transparent rounded-full" />
|
}`}>
|
||||||
) : (
|
{rule.action || "?"}
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
</span>
|
||||||
|
{/* Mobile: combined info on two lines */}
|
||||||
|
<div className="sm:hidden min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-xs text-blue-400 font-mono flex-shrink-0">{rule.p || "*"}</span>
|
||||||
|
<span className="text-xs text-muted-foreground flex-shrink-0">:</span>
|
||||||
|
<span className="text-xs text-foreground font-mono font-medium">{rule.dport || "*"}</span>
|
||||||
|
<span className={`text-[10px] px-1 py-0 rounded flex-shrink-0 ${
|
||||||
|
rule.source_file === "cluster" ? "bg-blue-500/10 text-blue-400" : "bg-purple-500/10 text-purple-400"
|
||||||
|
}`}>{rule.source_file}</span>
|
||||||
|
</div>
|
||||||
|
{comment && (
|
||||||
|
<p className="text-[10px] text-muted-foreground truncate mt-0.5">{comment}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Desktop: direction label */}
|
||||||
|
<span className="hidden sm:block text-xs text-muted-foreground font-mono">{direction}</span>
|
||||||
|
{/* Protocol */}
|
||||||
|
<span className="hidden sm:block text-xs text-blue-400 font-mono">{rule.p || "*"}</span>
|
||||||
|
{/* Port */}
|
||||||
|
<span className="hidden sm:block text-xs text-foreground font-mono font-medium">{rule.dport || "*"}</span>
|
||||||
|
{/* Source */}
|
||||||
|
<span className="hidden sm:block text-xs text-muted-foreground font-mono truncate">{rule.source || "any"}</span>
|
||||||
|
{/* Level badge */}
|
||||||
|
<span className={`hidden sm:block text-[10px] px-1.5 py-0.5 rounded text-center ${
|
||||||
|
rule.source_file === "cluster" ? "bg-blue-500/10 text-blue-400" : "bg-purple-500/10 text-purple-400"
|
||||||
|
}`}>
|
||||||
|
{rule.source_file}
|
||||||
|
</span>
|
||||||
|
{/* Expand/Delete */}
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<ChevronRight className={`h-3.5 w-3.5 text-muted-foreground transition-transform ${isExpanded ? "rotate-90" : ""}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded details */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="px-3 pb-3 pt-0 border-t border-border/50 bg-muted/10">
|
||||||
|
{editingRuleKey === ruleKey ? (
|
||||||
|
/* ── Inline Edit Form ── */
|
||||||
|
<div className="py-3 space-y-3">
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground uppercase">Direction</Label>
|
||||||
|
<select value={editRule.direction} onChange={(e) => setEditRule({ ...editRule, direction: e.target.value })}
|
||||||
|
className="w-full h-8 text-xs rounded-md border border-border bg-background px-2 mt-0.5">
|
||||||
|
<option value="IN">IN</option>
|
||||||
|
<option value="OUT">OUT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground uppercase">Action</Label>
|
||||||
|
<select value={editRule.action} onChange={(e) => setEditRule({ ...editRule, action: e.target.value })}
|
||||||
|
className="w-full h-8 text-xs rounded-md border border-border bg-background px-2 mt-0.5">
|
||||||
|
<option value="ACCEPT">ACCEPT</option>
|
||||||
|
<option value="DROP">DROP</option>
|
||||||
|
<option value="REJECT">REJECT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground uppercase">Protocol</Label>
|
||||||
|
<select value={editRule.protocol} onChange={(e) => setEditRule({ ...editRule, protocol: e.target.value })}
|
||||||
|
className="w-full h-8 text-xs rounded-md border border-border bg-background px-2 mt-0.5">
|
||||||
|
<option value="tcp">TCP</option>
|
||||||
|
<option value="udp">UDP</option>
|
||||||
|
<option value="icmp">ICMP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground uppercase">Port</Label>
|
||||||
|
<Input value={editRule.dport} onChange={(e) => setEditRule({ ...editRule, dport: e.target.value })}
|
||||||
|
placeholder="e.g. 80,443" className="h-8 text-xs mt-0.5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground uppercase">Source</Label>
|
||||||
|
<Input value={editRule.source} onChange={(e) => setEditRule({ ...editRule, source: e.target.value })}
|
||||||
|
placeholder="IP or CIDR" className="h-8 text-xs mt-0.5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground uppercase">Interface</Label>
|
||||||
|
<select value={editRule.iface} onChange={(e) => setEditRule({ ...editRule, iface: e.target.value })}
|
||||||
|
className="w-full h-8 text-xs rounded-md border border-border bg-background px-2 mt-0.5">
|
||||||
|
<option value="">Any</option>
|
||||||
|
{networkInterfaces.map((iface) => (
|
||||||
|
<option key={iface.name} value={iface.name}>
|
||||||
|
{iface.name} ({iface.type})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 sm:col-span-1">
|
||||||
|
<Label className="text-[10px] text-muted-foreground uppercase">Comment</Label>
|
||||||
|
<Input value={editRule.comment} onChange={(e) => setEditRule({ ...editRule, comment: e.target.value })}
|
||||||
|
placeholder="Description" className="h-8 text-xs mt-0.5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-1">
|
||||||
|
<Button variant="ghost" size="sm"
|
||||||
|
onClick={(e) => { e.stopPropagation(); setEditingRuleKey(null) }}
|
||||||
|
className="h-7 text-xs text-muted-foreground">
|
||||||
|
<X className="h-3 w-3 mr-1" /> Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleSaveEditRule(rule.rule_index, rule.source_file || "host") }}
|
||||||
|
disabled={savingRule}
|
||||||
|
className="h-7 text-xs text-green-500 border-green-500/30 hover:bg-green-500/10 bg-transparent">
|
||||||
|
{savingRule ? (
|
||||||
|
<div className="animate-spin h-3 w-3 border-2 border-green-500 border-t-transparent rounded-full mr-1" />
|
||||||
|
) : (
|
||||||
|
<Check className="h-3 w-3 mr-1" />
|
||||||
|
)}
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* ── Read-only Details ── */
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 py-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">Direction</p>
|
||||||
|
<p className="text-xs font-medium flex items-center gap-1">
|
||||||
|
{direction === "IN" ? <ArrowDownLeft className="h-3 w-3 text-blue-400" /> : <ArrowUpRight className="h-3 w-3 text-amber-400" />}
|
||||||
|
{direction === "IN" ? "Incoming" : "Outgoing"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">Protocol</p>
|
||||||
|
<p className="text-xs font-medium font-mono">{rule.p || "any"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">Port</p>
|
||||||
|
<p className="text-xs font-medium font-mono">{rule.dport || "any"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">Source</p>
|
||||||
|
<p className="text-xs font-medium font-mono">{rule.source || "any"}</p>
|
||||||
|
</div>
|
||||||
|
{rule.i && (
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">Interface</p>
|
||||||
|
<p className="text-xs font-medium font-mono">{rule.i}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">Scope</p>
|
||||||
|
<p className="text-xs font-medium flex items-center gap-1">
|
||||||
|
{rule.source_file === "cluster" ? <Globe className="h-3 w-3 text-blue-400" /> : <Shield className="h-3 w-3 text-purple-400" />}
|
||||||
|
{rule.source_file === "cluster" ? "Cluster" : "Host"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{comment && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">Comment</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{comment}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between pt-2 border-t border-border/50">
|
||||||
|
<code className="text-[10px] text-muted-foreground/60 font-mono truncate max-w-[50%]">{rule.raw}</code>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => { e.stopPropagation(); startEditRule(rule) }}
|
||||||
|
className="h-7 text-xs text-blue-400 border-blue-400/30 hover:bg-blue-400/10 bg-transparent"
|
||||||
|
>
|
||||||
|
<Pencil className="h-3 w-3 mr-1" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleDeleteRule(rule.rule_index, rule.source_file) }}
|
||||||
|
disabled={deletingRuleIdx === rule.rule_index}
|
||||||
|
className="h-7 text-xs text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent"
|
||||||
|
>
|
||||||
|
{deletingRuleIdx === rule.rule_index ? (
|
||||||
|
<div className="animate-spin h-3 w-3 border-2 border-red-500 border-t-transparent rounded-full mr-1" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="h-3 w-3 mr-1" />
|
||||||
|
)}
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
)
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -158,21 +158,15 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
||||||
try {
|
try {
|
||||||
let apiUrl = "/api/logs"
|
|
||||||
const params = new URLSearchParams()
|
|
||||||
|
|
||||||
const daysAgo = dateFilter === "custom" ? Number.parseInt(customDays) : Number.parseInt(dateFilter)
|
const daysAgo = dateFilter === "custom" ? Number.parseInt(customDays) : Number.parseInt(dateFilter)
|
||||||
// Clamp days to valid range
|
|
||||||
const clampedDays = Math.max(1, Math.min(daysAgo || 1, 90))
|
const clampedDays = Math.max(1, Math.min(daysAgo || 1, 90))
|
||||||
params.append("since_days", clampedDays.toString())
|
// Only send since_days - no limit param, so the backend returns ALL logs for the period
|
||||||
params.append("limit", "5000")
|
const apiUrl = `/api/logs?since_days=${clampedDays}`
|
||||||
|
|
||||||
if (params.toString()) {
|
|
||||||
apiUrl += `?${params.toString()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
console.log(`[v0] Fetching logs for ${clampedDays} days...`)
|
||||||
const data = await fetchApi(apiUrl)
|
const data = await fetchApi(apiUrl)
|
||||||
const logsArray = Array.isArray(data) ? data : data.logs || []
|
const logsArray = Array.isArray(data) ? data : data.logs || []
|
||||||
|
console.log(`[v0] Logs: parsed=${logsArray.length}, journal_total=${data.journal_total || 'N/A'}, skipped=${data.skipped || 0} for ${clampedDays} day(s)`)
|
||||||
return logsArray
|
return logsArray
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to load logs. Please try again.")
|
setError("Failed to load logs. Please try again.")
|
||||||
@@ -550,8 +544,13 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
if (loading && logs.length === 0 && events.length === 0) {
|
if (loading && logs.length === 0 && events.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||||
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
|
<div className="relative">
|
||||||
|
<div className="h-12 w-12 rounded-full border-2 border-muted"></div>
|
||||||
|
<div className="absolute inset-0 h-12 w-12 rounded-full border-2 border-transparent border-t-primary animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium text-foreground">Loading logs...</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Fetching system logs and events</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -559,11 +558,13 @@ export function SystemLogs() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{loading && (logs.length > 0 || events.length > 0) && (
|
{loading && (logs.length > 0 || events.length > 0) && (
|
||||||
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
<div className="fixed inset-0 bg-background/60 backdrop-blur-sm z-50 flex items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-4 p-8 rounded-lg bg-card border border-border shadow-lg">
|
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-card border border-border shadow-xl">
|
||||||
<RefreshCw className="h-12 w-12 animate-spin text-primary" />
|
<div className="relative">
|
||||||
<div className="text-lg font-medium text-foreground">Loading logs selected...</div>
|
<div className="h-10 w-10 rounded-full border-2 border-muted"></div>
|
||||||
<div className="text-sm text-muted-foreground">Please wait while we fetch the logs</div>
|
<div className="absolute inset-0 h-10 w-10 rounded-full border-2 border-transparent border-t-primary animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium text-foreground">Loading logs...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,10 +7,17 @@ import { Badge } from "./ui/badge"
|
|||||||
import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network } from "lucide-react"
|
import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network } from "lucide-react"
|
||||||
import { NodeMetricsCharts } from "./node-metrics-charts"
|
import { NodeMetricsCharts } from "./node-metrics-charts"
|
||||||
import { NetworkTrafficChart } from "./network-traffic-chart"
|
import { NetworkTrafficChart } from "./network-traffic-chart"
|
||||||
|
import { TemperatureDetailModal } from "./temperature-detail-modal"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
import { fetchApi } from "../lib/api-config"
|
import { fetchApi } from "../lib/api-config"
|
||||||
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
|
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
|
||||||
import { formatStorage } from "../lib/utils"
|
import { formatStorage } from "../lib/utils"
|
||||||
|
import { Area, AreaChart, ResponsiveContainer } from "recharts"
|
||||||
|
|
||||||
|
interface TempDataPoint {
|
||||||
|
timestamp: number
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
interface SystemData {
|
interface SystemData {
|
||||||
cpu_usage: number
|
cpu_usage: number
|
||||||
@@ -18,6 +25,7 @@ interface SystemData {
|
|||||||
memory_total: number
|
memory_total: number
|
||||||
memory_used: number
|
memory_used: number
|
||||||
temperature: number
|
temperature: number
|
||||||
|
temperature_sparkline?: TempDataPoint[]
|
||||||
uptime: string
|
uptime: string
|
||||||
load_average: number[]
|
load_average: number[]
|
||||||
hostname: string
|
hostname: string
|
||||||
@@ -178,6 +186,7 @@ export function SystemOverview() {
|
|||||||
const [networkTimeframe, setNetworkTimeframe] = useState("day")
|
const [networkTimeframe, setNetworkTimeframe] = useState("day")
|
||||||
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
||||||
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes") // Added networkUnit state
|
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes") // Added networkUnit state
|
||||||
|
const [tempModalOpen, setTempModalOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAllData = async () => {
|
const fetchAllData = async () => {
|
||||||
@@ -458,27 +467,60 @@ export function SystemOverview() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-card border-border">
|
<Card
|
||||||
|
className={`bg-card border-border ${systemData.temperature > 0 ? "cursor-pointer hover:bg-white/5 transition-colors" : ""}`}
|
||||||
|
onClick={() => systemData.temperature > 0 && setTempModalOpen(true)}
|
||||||
|
>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">Temperature</CardTitle>
|
<CardTitle className="text-sm font-medium text-muted-foreground">Temperature</CardTitle>
|
||||||
<Thermometer className="h-4 w-4 text-muted-foreground" />
|
<Thermometer className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-xl lg:text-2xl font-bold text-foreground">
|
<div className="flex items-center justify-between">
|
||||||
{systemData.temperature === 0 ? "N/A" : `${systemData.temperature}°C`}
|
<span className="text-xl lg:text-2xl font-bold text-foreground">
|
||||||
</div>
|
{systemData.temperature === 0 ? "N/A" : `${Math.round(systemData.temperature * 10) / 10}°C`}
|
||||||
<div className="flex items-center mt-2">
|
</span>
|
||||||
<Badge variant="outline" className={tempStatus.color}>
|
<Badge variant="outline" className={tempStatus.color}>
|
||||||
{tempStatus.status}
|
{tempStatus.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
{systemData.temperature > 0 && systemData.temperature_sparkline && systemData.temperature_sparkline.length > 1 ? (
|
||||||
{systemData.temperature === 0 ? "No sensor available" : "Live temperature reading"}
|
<div className="mt-2 h-10">
|
||||||
</p>
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<AreaChart data={systemData.temperature_sparkline} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="tempSparkGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor={systemData.temperature >= 75 ? "#ef4444" : systemData.temperature >= 60 ? "#f59e0b" : "#22c55e"} stopOpacity={0.3} />
|
||||||
|
<stop offset="100%" stopColor={systemData.temperature >= 75 ? "#ef4444" : systemData.temperature >= 60 ? "#f59e0b" : "#22c55e"} stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="value"
|
||||||
|
stroke={systemData.temperature >= 75 ? "#ef4444" : systemData.temperature >= 60 ? "#f59e0b" : "#22c55e"}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
fill="url(#tempSparkGradient)"
|
||||||
|
dot={false}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
{systemData.temperature === 0 ? "No sensor available" : "Collecting data..."}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TemperatureDetailModal
|
||||||
|
open={tempModalOpen}
|
||||||
|
onOpenChange={setTempModalOpen}
|
||||||
|
liveTemperature={systemData.temperature}
|
||||||
|
/>
|
||||||
|
|
||||||
<NodeMetricsCharts />
|
<NodeMetricsCharts />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ Provides REST API endpoints for authentication management
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
import auth_manager
|
import auth_manager
|
||||||
import jwt
|
import jwt
|
||||||
@@ -73,12 +77,36 @@ def ssl_status():
|
|||||||
return jsonify({"success": False, "message": str(e)}), 500
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
def _schedule_service_restart(delay=1.5):
|
||||||
|
"""Schedule a restart of the monitor service via systemctl after a short delay.
|
||||||
|
This gives time for the HTTP response to reach the client before the process restarts."""
|
||||||
|
def _do_restart():
|
||||||
|
time.sleep(delay)
|
||||||
|
print("[ProxMenux] Restarting monitor service to apply SSL changes...")
|
||||||
|
# Use systemctl restart which properly stops and starts the service.
|
||||||
|
# This works because systemd manages proxmenux-monitor.service.
|
||||||
|
try:
|
||||||
|
subprocess.Popen(
|
||||||
|
["systemctl", "restart", "proxmenux-monitor"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ProxMenux] Failed to restart via systemctl: {e}")
|
||||||
|
# Fallback: try to restart the process directly
|
||||||
|
os.kill(os.getpid(), 15) # SIGTERM
|
||||||
|
|
||||||
|
t = threading.Thread(target=_do_restart, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/api/ssl/configure', methods=['POST'])
|
@auth_bp.route('/api/ssl/configure', methods=['POST'])
|
||||||
def ssl_configure():
|
def ssl_configure():
|
||||||
"""Configure SSL with Proxmox or custom certificates"""
|
"""Configure SSL with Proxmox or custom certificates"""
|
||||||
try:
|
try:
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
source = data.get("source", "proxmox")
|
source = data.get("source", "proxmox")
|
||||||
|
auto_restart = data.get("auto_restart", True)
|
||||||
|
|
||||||
if source == "proxmox":
|
if source == "proxmox":
|
||||||
cert_path = auth_manager.PROXMOX_CERT_PATH
|
cert_path = auth_manager.PROXMOX_CERT_PATH
|
||||||
@@ -92,7 +120,14 @@ def ssl_configure():
|
|||||||
success, message = auth_manager.configure_ssl(cert_path, key_path, source)
|
success, message = auth_manager.configure_ssl(cert_path, key_path, source)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return jsonify({"success": True, "message": message, "requires_restart": True})
|
if auto_restart:
|
||||||
|
_schedule_service_restart()
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "SSL enabled. The service is restarting...",
|
||||||
|
"restarting": auto_restart,
|
||||||
|
"new_protocol": "https"
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
return jsonify({"success": False, "message": message}), 400
|
return jsonify({"success": False, "message": message}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -103,10 +138,20 @@ def ssl_configure():
|
|||||||
def ssl_disable():
|
def ssl_disable():
|
||||||
"""Disable SSL and return to HTTP"""
|
"""Disable SSL and return to HTTP"""
|
||||||
try:
|
try:
|
||||||
|
data = request.json or {}
|
||||||
|
auto_restart = data.get("auto_restart", True)
|
||||||
|
|
||||||
success, message = auth_manager.disable_ssl()
|
success, message = auth_manager.disable_ssl()
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return jsonify({"success": True, "message": message, "requires_restart": True})
|
if auto_restart:
|
||||||
|
_schedule_service_restart()
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "SSL disabled. The service is restarting...",
|
||||||
|
"restarting": auto_restart,
|
||||||
|
"new_protocol": "http"
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
return jsonify({"success": False, "message": message}), 400
|
return jsonify({"success": False, "message": message}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -106,6 +106,39 @@ def firewall_delete_rule():
|
|||||||
return jsonify({"success": False, "message": str(e)}), 500
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@security_bp.route('/api/security/firewall/rules/edit', methods=['PUT'])
|
||||||
|
def firewall_edit_rule():
|
||||||
|
"""Edit an existing firewall rule (delete old + insert new at same position)"""
|
||||||
|
if not security_manager:
|
||||||
|
return jsonify({"success": False, "message": "Security manager not available"}), 500
|
||||||
|
try:
|
||||||
|
data = request.json or {}
|
||||||
|
rule_index = data.get("rule_index")
|
||||||
|
level = data.get("level", "host")
|
||||||
|
new_rule = data.get("new_rule", {})
|
||||||
|
if rule_index is None:
|
||||||
|
return jsonify({"success": False, "message": "rule_index is required"}), 400
|
||||||
|
|
||||||
|
success, message = security_manager.edit_firewall_rule(
|
||||||
|
rule_index=int(rule_index),
|
||||||
|
level=level,
|
||||||
|
direction=new_rule.get("direction", "IN"),
|
||||||
|
action=new_rule.get("action", "ACCEPT"),
|
||||||
|
protocol=new_rule.get("protocol", "tcp"),
|
||||||
|
dport=new_rule.get("dport", ""),
|
||||||
|
sport=new_rule.get("sport", ""),
|
||||||
|
source=new_rule.get("source", ""),
|
||||||
|
iface=new_rule.get("iface", ""),
|
||||||
|
comment=new_rule.get("comment", ""),
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@security_bp.route('/api/security/firewall/monitor-port', methods=['POST'])
|
@security_bp.route('/api/security/firewall/monitor-port', methods=['POST'])
|
||||||
def firewall_add_monitor_port():
|
def firewall_add_monitor_port():
|
||||||
"""Add firewall rule to allow port 8008 for ProxMenux Monitor"""
|
"""Add firewall rule to allow port 8008 for ProxMenux Monitor"""
|
||||||
|
|||||||
@@ -274,6 +274,96 @@ def add_firewall_rule(direction="IN", action="ACCEPT", protocol="tcp", dport="",
|
|||||||
return False, f"Failed to add firewall rule: {str(e)}"
|
return False, f"Failed to add firewall rule: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
def edit_firewall_rule(rule_index, level="host", direction="IN", action="ACCEPT",
|
||||||
|
protocol="tcp", dport="", sport="", source="", iface="", comment=""):
|
||||||
|
"""
|
||||||
|
Edit an existing firewall rule by replacing it in-place.
|
||||||
|
Deletes the old rule at rule_index and inserts the new one at the same position.
|
||||||
|
Returns (success, message)
|
||||||
|
"""
|
||||||
|
# Validate inputs
|
||||||
|
action = action.upper()
|
||||||
|
if action not in ("ACCEPT", "DROP", "REJECT"):
|
||||||
|
return False, f"Invalid action: {action}. Must be ACCEPT, DROP, or REJECT"
|
||||||
|
direction = direction.upper()
|
||||||
|
if direction not in ("IN", "OUT"):
|
||||||
|
return False, f"Invalid direction: {direction}. Must be IN or OUT"
|
||||||
|
|
||||||
|
# Build new rule line
|
||||||
|
parts = [direction, action]
|
||||||
|
if protocol:
|
||||||
|
parts.extend(["-p", protocol.lower()])
|
||||||
|
if dport:
|
||||||
|
if not re.match(r'^[\d:,]+$', dport):
|
||||||
|
return False, f"Invalid destination port: {dport}"
|
||||||
|
parts.extend(["-dport", dport])
|
||||||
|
if sport:
|
||||||
|
if not re.match(r'^[\d:,]+$', sport):
|
||||||
|
return False, f"Invalid source port: {sport}"
|
||||||
|
parts.extend(["-sport", sport])
|
||||||
|
if source:
|
||||||
|
parts.extend(["-source", source])
|
||||||
|
if iface:
|
||||||
|
parts.extend(["-i", iface])
|
||||||
|
parts.extend(["-log", "nolog"])
|
||||||
|
if comment:
|
||||||
|
safe_comment = re.sub(r'[^\w\s\-._/():]', '', comment)
|
||||||
|
parts.append(f"# {safe_comment}")
|
||||||
|
new_rule_line = " ".join(parts)
|
||||||
|
|
||||||
|
# Determine target file
|
||||||
|
if level == "cluster":
|
||||||
|
fw_file = CLUSTER_FW
|
||||||
|
else:
|
||||||
|
fw_file = os.path.join(HOST_FW_DIR, "host.fw")
|
||||||
|
|
||||||
|
if not os.path.isfile(fw_file):
|
||||||
|
return False, "Firewall config file not found"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(fw_file, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
lines = content.splitlines()
|
||||||
|
new_lines = []
|
||||||
|
in_rules = False
|
||||||
|
current_rule_idx = 0
|
||||||
|
replaced = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith('['):
|
||||||
|
section_match = re.match(r'\[(\w+)\]', stripped)
|
||||||
|
if section_match:
|
||||||
|
section = section_match.group(1).upper()
|
||||||
|
in_rules = section in ("RULES", "IN", "OUT")
|
||||||
|
|
||||||
|
if in_rules and stripped and not stripped.startswith('#') and not stripped.startswith('['):
|
||||||
|
if current_rule_idx == rule_index:
|
||||||
|
# Replace the old rule with the new one
|
||||||
|
new_lines.append(new_rule_line)
|
||||||
|
replaced = True
|
||||||
|
current_rule_idx += 1
|
||||||
|
continue
|
||||||
|
current_rule_idx += 1
|
||||||
|
|
||||||
|
new_lines.append(line)
|
||||||
|
|
||||||
|
if not replaced:
|
||||||
|
return False, f"Rule index {rule_index} not found"
|
||||||
|
|
||||||
|
with open(fw_file, 'w') as f:
|
||||||
|
f.write("\n".join(new_lines) + "\n")
|
||||||
|
|
||||||
|
_run_cmd(["pve-firewall", "reload"])
|
||||||
|
|
||||||
|
return True, f"Firewall rule updated: {direction} {action} {protocol}{':' + dport if dport else ''}"
|
||||||
|
except PermissionError:
|
||||||
|
return False, "Permission denied. Cannot modify firewall config."
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Failed to edit rule: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
def delete_firewall_rule(rule_index, level="host"):
|
def delete_firewall_rule(rule_index, level="host"):
|
||||||
"""
|
"""
|
||||||
Delete a firewall rule by index from host or cluster config.
|
Delete a firewall rule by index from host or cluster config.
|
||||||
|
|||||||
@@ -238,8 +238,12 @@ Seal=no
|
|||||||
Compress=yes
|
Compress=yes
|
||||||
SystemMaxUse=64M
|
SystemMaxUse=64M
|
||||||
RuntimeMaxUse=60M
|
RuntimeMaxUse=60M
|
||||||
MaxLevelStore=warning
|
# MaxLevelStore=info allows ProxMenux Monitor to display system logs correctly.
|
||||||
MaxLevelSyslog=warning
|
# Using "warning" causes the log viewer to show nearly identical entries across
|
||||||
|
# all date ranges (1d/3d/7d) because most activity is info-level.
|
||||||
|
# It also prevents Fail2Ban from detecting SSH/Proxmox auth failures via journal.
|
||||||
|
MaxLevelStore=info
|
||||||
|
MaxLevelSyslog=info
|
||||||
MaxLevelKMsg=warning
|
MaxLevelKMsg=warning
|
||||||
MaxLevelConsole=notice
|
MaxLevelConsole=notice
|
||||||
MaxLevelWall=crit
|
MaxLevelWall=crit
|
||||||
@@ -787,8 +791,10 @@ Compress=yes
|
|||||||
SystemMaxUse=${USE_MB}M
|
SystemMaxUse=${USE_MB}M
|
||||||
SystemKeepFree=${KEEP_MB}M
|
SystemKeepFree=${KEEP_MB}M
|
||||||
RuntimeMaxUse=${RUNTIME_MB}M
|
RuntimeMaxUse=${RUNTIME_MB}M
|
||||||
MaxLevelStore=warning
|
# MaxLevelStore=info: required for ProxMenux Monitor log display and Fail2Ban detection.
|
||||||
MaxLevelSyslog=warning
|
# Using "warning" silently discards most system logs making date filters useless.
|
||||||
|
MaxLevelStore=info
|
||||||
|
MaxLevelSyslog=info
|
||||||
MaxLevelKMsg=warning
|
MaxLevelKMsg=warning
|
||||||
MaxLevelConsole=notice
|
MaxLevelConsole=notice
|
||||||
MaxLevelWall=crit
|
MaxLevelWall=crit
|
||||||
|
|||||||
Reference in New Issue
Block a user