mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 08:56:21 +00:00
Update notification service
This commit is contained in:
@@ -85,6 +85,57 @@ export function ProxmoxDashboard() {
|
|||||||
const [showHealthModal, setShowHealthModal] = useState(false)
|
const [showHealthModal, setShowHealthModal] = useState(false)
|
||||||
const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck()
|
const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck()
|
||||||
|
|
||||||
|
// Category keys for health info count calculation
|
||||||
|
const HEALTH_CATEGORY_KEYS = [
|
||||||
|
{ key: "cpu", category: "temperature" },
|
||||||
|
{ key: "memory", category: "memory" },
|
||||||
|
{ key: "storage", category: "storage" },
|
||||||
|
{ key: "disks", category: "disks" },
|
||||||
|
{ key: "network", category: "network" },
|
||||||
|
{ key: "vms", category: "vms" },
|
||||||
|
{ key: "services", category: "pve_services" },
|
||||||
|
{ key: "logs", category: "logs" },
|
||||||
|
{ key: "updates", category: "updates" },
|
||||||
|
{ key: "security", category: "security" },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Fetch health info count independently (for initial load and refresh)
|
||||||
|
const fetchHealthInfoCount = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetchApi("/api/health/full")
|
||||||
|
let calculatedInfoCount = 0
|
||||||
|
|
||||||
|
if (response && response.health?.details) {
|
||||||
|
// Get categories that have dismissed items (these become INFO)
|
||||||
|
const customCats = new Set((response.custom_suppressions || []).map((cs: { category: string }) => cs.category))
|
||||||
|
const filteredDismissed = (response.dismissed || []).filter((item: { category: string }) => !customCats.has(item.category))
|
||||||
|
const categoriesWithDismissed = new Set<string>()
|
||||||
|
filteredDismissed.forEach((item: { category: string }) => {
|
||||||
|
const catMeta = HEALTH_CATEGORY_KEYS.find(c => c.category === item.category || c.key === item.category)
|
||||||
|
if (catMeta) {
|
||||||
|
categoriesWithDismissed.add(catMeta.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Count effective INFO categories (original INFO + OK categories with dismissed)
|
||||||
|
HEALTH_CATEGORY_KEYS.forEach(({ key }) => {
|
||||||
|
const cat = response.health.details[key as keyof typeof response.health.details]
|
||||||
|
if (cat) {
|
||||||
|
const originalStatus = cat.status?.toUpperCase()
|
||||||
|
// Count as INFO if: originally INFO OR (originally OK and has dismissed items)
|
||||||
|
if (originalStatus === "INFO" || (originalStatus === "OK" && categoriesWithDismissed.has(key))) {
|
||||||
|
calculatedInfoCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setInfoCount(calculatedInfoCount)
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail - infoCount will remain at 0
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const fetchSystemData = useCallback(async () => {
|
const fetchSystemData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data: FlaskSystemInfo = await fetchApi("/api/system-info")
|
const data: FlaskSystemInfo = await fetchApi("/api/system-info")
|
||||||
@@ -129,20 +180,25 @@ export function ProxmoxDashboard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Siempre fetch inicial
|
// Siempre fetch inicial
|
||||||
fetchSystemData()
|
fetchSystemData()
|
||||||
|
fetchHealthInfoCount() // Fetch info count on initial load
|
||||||
|
|
||||||
// En overview: cada 30 segundos para actualización frecuente del estado de salud
|
// En overview: cada 30 segundos para actualización frecuente del estado de salud
|
||||||
// En otras tabs: cada 60 segundos para reducir carga
|
// En otras tabs: cada 60 segundos para reducir carga
|
||||||
let interval: ReturnType<typeof setInterval> | null = null
|
let interval: ReturnType<typeof setInterval> | null = null
|
||||||
|
let healthInterval: ReturnType<typeof setInterval> | null = null
|
||||||
if (activeTab === "overview") {
|
if (activeTab === "overview") {
|
||||||
interval = setInterval(fetchSystemData, 30000) // 30 segundos
|
interval = setInterval(fetchSystemData, 30000) // 30 segundos
|
||||||
|
healthInterval = setInterval(fetchHealthInfoCount, 30000) // Also refresh info count
|
||||||
} else {
|
} else {
|
||||||
interval = setInterval(fetchSystemData, 60000) // 60 segundos
|
interval = setInterval(fetchSystemData, 60000) // 60 segundos
|
||||||
|
healthInterval = setInterval(fetchHealthInfoCount, 60000) // Also refresh info count
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (interval) clearInterval(interval)
|
if (interval) clearInterval(interval)
|
||||||
|
if (healthInterval) clearInterval(healthInterval)
|
||||||
}
|
}
|
||||||
}, [fetchSystemData, activeTab])
|
}, [fetchSystemData, fetchHealthInfoCount, activeTab])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleChangeTab = (event: CustomEvent) => {
|
const handleChangeTab = (event: CustomEvent) => {
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ class HealthPersistence:
|
|||||||
if 'suppression_hours' not in columns:
|
if 'suppression_hours' not in columns:
|
||||||
cursor.execute('ALTER TABLE errors ADD COLUMN suppression_hours INTEGER DEFAULT 24')
|
cursor.execute('ALTER TABLE errors ADD COLUMN suppression_hours INTEGER DEFAULT 24')
|
||||||
|
|
||||||
|
# Migration: add acknowledged_at column to errors if not present
|
||||||
|
if 'acknowledged_at' not in columns:
|
||||||
|
cursor.execute('ALTER TABLE errors ADD COLUMN acknowledged_at TEXT')
|
||||||
|
|
||||||
# Indexes for performance
|
# Indexes for performance
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_error_key ON errors(error_key)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_error_key ON errors(error_key)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON errors(category)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON errors(category)')
|
||||||
@@ -346,9 +350,10 @@ class HealthPersistence:
|
|||||||
configured_hours = int(stored)
|
configured_hours = int(stored)
|
||||||
if configured_hours != self.DEFAULT_SUPPRESSION_HOURS:
|
if configured_hours != self.DEFAULT_SUPPRESSION_HOURS:
|
||||||
# Non-default setting found: auto-acknowledge
|
# Non-default setting found: auto-acknowledge
|
||||||
|
# Mark as acknowledged but DO NOT set resolved_at - error remains active
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE errors
|
UPDATE errors
|
||||||
SET acknowledged = 1, resolved_at = ?, suppression_hours = ?
|
SET acknowledged = 1, acknowledged_at = ?, suppression_hours = ?
|
||||||
WHERE error_key = ? AND acknowledged = 0
|
WHERE error_key = ? AND acknowledged = 0
|
||||||
''', (now, configured_hours, error_key))
|
''', (now, configured_hours, error_key))
|
||||||
|
|
||||||
@@ -514,9 +519,10 @@ class HealthPersistence:
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Insert as acknowledged but NOT resolved - error remains active
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO errors (error_key, category, severity, reason, first_seen, last_seen,
|
INSERT INTO errors (error_key, category, severity, reason, first_seen, last_seen,
|
||||||
occurrence_count, acknowledged, resolved_at, suppression_hours)
|
occurrence_count, acknowledged, acknowledged_at, suppression_hours)
|
||||||
VALUES (?, ?, 'WARNING', 'Dismissed by user', ?, ?, 1, 1, ?, ?)
|
VALUES (?, ?, 'WARNING', 'Dismissed by user', ?, ?, 1, 1, ?, ?)
|
||||||
''', (error_key, category, now, now, now, sup_hours))
|
''', (error_key, category, now, now, now, sup_hours))
|
||||||
|
|
||||||
@@ -554,9 +560,12 @@ class HealthPersistence:
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Mark as acknowledged but DO NOT set resolved_at
|
||||||
|
# The error remains active until it actually disappears from the system
|
||||||
|
# resolved_at should only be set when the error is truly resolved
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE errors
|
UPDATE errors
|
||||||
SET acknowledged = 1, resolved_at = ?, suppression_hours = ?
|
SET acknowledged = 1, acknowledged_at = ?, suppression_hours = ?
|
||||||
WHERE error_key = ?
|
WHERE error_key = ?
|
||||||
''', (now, sup_hours, error_key))
|
''', (now, sup_hours, error_key))
|
||||||
|
|
||||||
@@ -578,9 +587,10 @@ class HealthPersistence:
|
|||||||
if child_prefix:
|
if child_prefix:
|
||||||
# Only cascade to active (unresolved) child errors.
|
# Only cascade to active (unresolved) child errors.
|
||||||
# Already-resolved/expired entries must NOT be re-surfaced.
|
# Already-resolved/expired entries must NOT be re-surfaced.
|
||||||
|
# Mark as acknowledged but DO NOT set resolved_at
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE errors
|
UPDATE errors
|
||||||
SET acknowledged = 1, resolved_at = ?, suppression_hours = ?
|
SET acknowledged = 1, acknowledged_at = ?, suppression_hours = ?
|
||||||
WHERE error_key LIKE ? AND acknowledged = 0 AND resolved_at IS NULL
|
WHERE error_key LIKE ? AND acknowledged = 0 AND resolved_at IS NULL
|
||||||
''', (now, sup_hours, child_prefix + '%'))
|
''', (now, sup_hours, child_prefix + '%'))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user