mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update health monitor
This commit is contained in:
@@ -55,8 +55,10 @@ export function Settings() {
|
|||||||
// Health Monitor suppression settings
|
// Health Monitor suppression settings
|
||||||
const [suppressionCategories, setSuppressionCategories] = useState<SuppressionCategory[]>([])
|
const [suppressionCategories, setSuppressionCategories] = useState<SuppressionCategory[]>([])
|
||||||
const [loadingHealth, setLoadingHealth] = useState(true)
|
const [loadingHealth, setLoadingHealth] = useState(true)
|
||||||
const [savingHealth, setSavingHealth] = useState<string | null>(null)
|
const [healthEditMode, setHealthEditMode] = useState(false)
|
||||||
const [savedHealth, setSavedHealth] = useState<string | null>(null)
|
const [savingAllHealth, setSavingAllHealth] = useState(false)
|
||||||
|
const [savedAllHealth, setSavedAllHealth] = useState(false)
|
||||||
|
const [pendingChanges, setPendingChanges] = useState<Record<string, number>>({})
|
||||||
const [customValues, setCustomValues] = useState<Record<string, string>>({})
|
const [customValues, setCustomValues] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -118,58 +120,98 @@ export function Settings() {
|
|||||||
return "custom"
|
return "custom"
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSuppressionChange = async (settingKey: string, value: string) => {
|
const getEffectiveHours = (cat: SuppressionCategory): number => {
|
||||||
|
if (cat.key in pendingChanges) return pendingChanges[cat.key]
|
||||||
|
return cat.hours
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSuppressionChange = (settingKey: string, value: string) => {
|
||||||
if (value === "custom") {
|
if (value === "custom") {
|
||||||
// Show custom input -- don't save yet
|
|
||||||
const current = suppressionCategories.find(c => c.key === settingKey)
|
const current = suppressionCategories.find(c => c.key === settingKey)
|
||||||
setCustomValues(prev => ({ ...prev, [settingKey]: String(current?.hours || 48) }))
|
const effectiveHours = current ? getEffectiveHours(current) : 48
|
||||||
// Temporarily mark as custom in state
|
setCustomValues(prev => ({ ...prev, [settingKey]: String(effectiveHours > 0 ? effectiveHours : 48) }))
|
||||||
setSuppressionCategories(prev =>
|
// Mark as custom mode in pending
|
||||||
prev.map(c => c.key === settingKey ? { ...c, hours: -2 } : c)
|
setPendingChanges(prev => ({ ...prev, [settingKey]: -2 }))
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const hours = parseInt(value, 10)
|
const hours = parseInt(value, 10)
|
||||||
if (isNaN(hours)) return
|
if (isNaN(hours)) return
|
||||||
|
setPendingChanges(prev => ({ ...prev, [settingKey]: hours }))
|
||||||
await saveSuppression(settingKey, hours)
|
// Clear custom input if switching away
|
||||||
|
setCustomValues(prev => {
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[settingKey]
|
||||||
|
return next
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCustomSave = async (settingKey: string) => {
|
const handleCustomConfirm = (settingKey: string) => {
|
||||||
const raw = customValues[settingKey]
|
const raw = customValues[settingKey]
|
||||||
const hours = parseInt(raw, 10)
|
const hours = parseInt(raw, 10)
|
||||||
if (isNaN(hours) || hours < 1) return
|
if (isNaN(hours) || hours < 1) return
|
||||||
await saveSuppression(settingKey, hours)
|
setPendingChanges(prev => ({ ...prev, [settingKey]: hours }))
|
||||||
|
setCustomValues(prev => {
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[settingKey]
|
||||||
|
return next
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveSuppression = async (settingKey: string, hours: number) => {
|
const handleCancelEdit = () => {
|
||||||
setSavingHealth(settingKey)
|
setHealthEditMode(false)
|
||||||
|
setPendingChanges({})
|
||||||
|
setCustomValues({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveAllHealth = async () => {
|
||||||
|
// Merge pending changes into a payload: only changed categories
|
||||||
|
const payload: Record<string, string> = {}
|
||||||
|
for (const cat of suppressionCategories) {
|
||||||
|
if (cat.key in pendingChanges && pendingChanges[cat.key] !== -2) {
|
||||||
|
payload[cat.key] = String(pendingChanges[cat.key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(payload).length === 0) {
|
||||||
|
setHealthEditMode(false)
|
||||||
|
setPendingChanges({})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSavingAllHealth(true)
|
||||||
try {
|
try {
|
||||||
await fetchApi("/api/health/settings", {
|
await fetchApi("/api/health/settings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ [settingKey]: String(hours) }),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update local state with saved values
|
||||||
setSuppressionCategories(prev =>
|
setSuppressionCategories(prev =>
|
||||||
prev.map(c => c.key === settingKey ? { ...c, hours } : c)
|
prev.map(c => {
|
||||||
|
if (c.key in pendingChanges && pendingChanges[c.key] !== -2) {
|
||||||
|
return { ...c, hours: pendingChanges[c.key] }
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
})
|
||||||
)
|
)
|
||||||
// Remove from custom values
|
setPendingChanges({})
|
||||||
setCustomValues(prev => {
|
setCustomValues({})
|
||||||
const next = { ...prev }
|
setHealthEditMode(false)
|
||||||
delete next[settingKey]
|
setSavedAllHealth(true)
|
||||||
return next
|
setTimeout(() => setSavedAllHealth(false), 3000)
|
||||||
})
|
|
||||||
setSavedHealth(settingKey)
|
|
||||||
setTimeout(() => setSavedHealth(null), 2000)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to save health setting:", err)
|
console.error("Failed to save health settings:", err)
|
||||||
} finally {
|
} finally {
|
||||||
setSavingHealth(null)
|
setSavingAllHealth(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasPendingChanges = Object.keys(pendingChanges).some(
|
||||||
|
k => pendingChanges[k] !== -2
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -211,13 +253,56 @@ export function Settings() {
|
|||||||
{/* Health Monitor Settings */}
|
{/* Health Monitor Settings */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<HeartPulse className="h-5 w-5 text-red-500" />
|
<div className="flex items-center gap-2">
|
||||||
<CardTitle>Health Monitor</CardTitle>
|
<HeartPulse className="h-5 w-5 text-red-500" />
|
||||||
|
<CardTitle>Health Monitor</CardTitle>
|
||||||
|
</div>
|
||||||
|
{!loadingHealth && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{savedAllHealth && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-green-500">
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
Saved
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{healthEditMode ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
disabled={savingAllHealth}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="h-7 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center gap-1.5"
|
||||||
|
onClick={handleSaveAllHealth}
|
||||||
|
disabled={savingAllHealth || !hasPendingChanges}
|
||||||
|
>
|
||||||
|
{savingAllHealth ? (
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1.5"
|
||||||
|
onClick={() => setHealthEditMode(true)}
|
||||||
|
>
|
||||||
|
<Settings2 className="h-3 w-3" />
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configure how long dismissed alerts stay suppressed for each category.
|
Configure how long dismissed alerts stay suppressed for each category.
|
||||||
When you dismiss a warning, it will not reappear until the suppression period expires.
|
Changes apply immediately to both existing and future dismissed alerts.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -226,116 +311,124 @@ export function Settings() {
|
|||||||
<div className="animate-spin h-8 w-8 border-4 border-red-500 border-t-transparent rounded-full" />
|
<div className="animate-spin h-8 w-8 border-4 border-red-500 border-t-transparent rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-0">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-3 pb-2 border-b border-border">
|
<div className="flex items-center justify-between pb-2 mb-1 border-b border-border">
|
||||||
<span className="text-sm font-medium text-muted-foreground">Category</span>
|
<span className="text-xs font-medium text-muted-foreground">Category</span>
|
||||||
<span className="text-sm font-medium text-muted-foreground">Suppression Duration</span>
|
<span className="text-xs font-medium text-muted-foreground">Suppression Duration</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Per-category rows */}
|
{/* Per-category rows */}
|
||||||
{suppressionCategories.map((cat) => {
|
<div className="divide-y divide-border/50">
|
||||||
const IconComp = CATEGORY_ICONS[cat.icon] || HeartPulse
|
{suppressionCategories.map((cat) => {
|
||||||
const isCustomMode = cat.hours === -2 || (cat.key in customValues)
|
const IconComp = CATEGORY_ICONS[cat.icon] || HeartPulse
|
||||||
const isPermanent = cat.hours === -1
|
const effectiveHours = getEffectiveHours(cat)
|
||||||
const isLong = cat.hours >= 720 && cat.hours !== -1
|
const isCustomMode = effectiveHours === -2 || (cat.key in customValues)
|
||||||
const selectVal = isCustomMode ? "custom" : getSelectValue(cat.hours, cat.key)
|
const isPermanent = effectiveHours === -1
|
||||||
|
const isLong = effectiveHours >= 720 && effectiveHours !== -1 && effectiveHours !== -2
|
||||||
return (
|
const hasChanged = cat.key in pendingChanges && pendingChanges[cat.key] !== cat.hours
|
||||||
<div key={cat.key} className="space-y-0">
|
const selectVal = isCustomMode ? "custom" : getSelectValue(effectiveHours, cat.key)
|
||||||
<div className="flex items-center justify-between gap-3 py-2.5 px-2 rounded-lg hover:bg-muted/30 transition-colors">
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
return (
|
||||||
<IconComp className="h-4 w-4 text-muted-foreground shrink-0" />
|
<div key={cat.key}>
|
||||||
<span className="text-sm font-medium truncate">{cat.label}</span>
|
<div className="flex items-center justify-between gap-2 py-2 sm:py-2.5 px-1 sm:px-2">
|
||||||
{savingHealth === cat.key && (
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground shrink-0" />
|
<IconComp className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
)}
|
<span className="text-xs sm:text-sm font-medium">{cat.label}</span>
|
||||||
{savedHealth === cat.key && (
|
{hasChanged && healthEditMode && (
|
||||||
<Check className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
<span className="h-1.5 w-1.5 rounded-full bg-blue-500 shrink-0" />
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
|
||||||
{isCustomMode ? (
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
className="w-20 h-8 text-xs"
|
|
||||||
value={customValues[cat.key] || ""}
|
|
||||||
onChange={(e) => setCustomValues(prev => ({ ...prev, [cat.key]: e.target.value }))}
|
|
||||||
placeholder="Hours"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-muted-foreground">h</span>
|
|
||||||
<button
|
|
||||||
className="h-8 px-2 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors"
|
|
||||||
onClick={() => handleCustomSave(cat.key)}
|
|
||||||
disabled={savingHealth === cat.key}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="h-8 px-2 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
|
|
||||||
onClick={() => {
|
|
||||||
setCustomValues(prev => {
|
|
||||||
const next = { ...prev }
|
|
||||||
delete next[cat.key]
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
loadHealthSettings()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Select value={selectVal} onValueChange={(v) => handleSuppressionChange(cat.key, v)}>
|
|
||||||
<SelectTrigger className="w-32 h-8 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{SUPPRESSION_OPTIONS.map((opt) => (
|
|
||||||
<SelectItem key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Warning for Permanent */}
|
|
||||||
{isPermanent && (
|
|
||||||
<div className="flex items-start gap-2 ml-8 mr-2 mb-2 p-2.5 rounded-md bg-amber-500/10 border border-amber-500/20">
|
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-500 shrink-0 mt-0.5" />
|
|
||||||
<p className="text-xs text-amber-400/90 leading-relaxed">
|
|
||||||
Dismissed alerts for <span className="font-semibold">{cat.label}</span> will never reappear.
|
|
||||||
{cat.category === "temperature" && (
|
|
||||||
<span className="block mt-1 text-amber-300 font-medium">
|
|
||||||
Note: Critical CPU temperature alerts will still trigger for hardware safety.
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
|
<div className="shrink-0">
|
||||||
|
{isCustomMode && healthEditMode ? (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
className="w-16 sm:w-20 h-7 text-xs"
|
||||||
|
value={customValues[cat.key] || ""}
|
||||||
|
onChange={(e) => setCustomValues(prev => ({ ...prev, [cat.key]: e.target.value }))}
|
||||||
|
placeholder="Hours"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">h</span>
|
||||||
|
<button
|
||||||
|
className="h-7 px-2 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors"
|
||||||
|
onClick={() => handleCustomConfirm(cat.key)}
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="h-7 px-1.5 text-xs rounded-md text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
setCustomValues(prev => {
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[cat.key]
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
setPendingChanges(prev => {
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[cat.key]
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={selectVal}
|
||||||
|
onValueChange={(v) => handleSuppressionChange(cat.key, v)}
|
||||||
|
disabled={!healthEditMode}
|
||||||
|
>
|
||||||
|
<SelectTrigger className={`w-28 sm:w-32 h-7 text-xs ${!healthEditMode ? "opacity-60" : ""}`}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{SUPPRESSION_OPTIONS.map((opt) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* Notice for Permanent */}
|
||||||
{/* Warning for long custom duration (> 1 month) */}
|
{isPermanent && healthEditMode && (
|
||||||
{isLong && !isPermanent && (
|
<div className="flex items-start gap-2 ml-6 sm:ml-8 mr-1 mb-2 p-2 rounded-md bg-blue-500/10 border border-blue-500/20">
|
||||||
<div className="flex items-start gap-2 ml-8 mr-2 mb-2 p-2.5 rounded-md bg-amber-500/10 border border-amber-500/20">
|
<Info className="h-3.5 w-3.5 text-blue-400 shrink-0 mt-0.5" />
|
||||||
<Info className="h-4 w-4 text-amber-500 shrink-0 mt-0.5" />
|
<p className="text-[11px] text-blue-400/90 leading-relaxed">
|
||||||
<p className="text-xs text-amber-400/90 leading-relaxed">
|
Alerts for <span className="font-semibold">{cat.label}</span> will be permanently suppressed when dismissed.
|
||||||
Long suppression period. Dismissed alerts for this category will not reappear for an extended time.
|
{cat.category === "temperature" && (
|
||||||
</p>
|
<span className="block mt-0.5 text-blue-300/80">
|
||||||
</div>
|
Critical CPU temperature alerts will still trigger for hardware safety.
|
||||||
)}
|
</span>
|
||||||
</div>
|
)}
|
||||||
)
|
</p>
|
||||||
})}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notice for long duration (> 1 month) */}
|
||||||
|
{isLong && healthEditMode && (
|
||||||
|
<div className="flex items-start gap-2 ml-6 sm:ml-8 mr-1 mb-2 p-2 rounded-md bg-blue-500/10 border border-blue-500/20">
|
||||||
|
<Info className="h-3.5 w-3.5 text-blue-400 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-[11px] text-blue-400/90 leading-relaxed">
|
||||||
|
Long suppression period. Dismissed alerts for this category will not reappear for an extended time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Info footer */}
|
{/* Info footer */}
|
||||||
<div className="flex items-start gap-2 mt-4 pt-3 border-t border-border">
|
<div className="flex items-start gap-2 mt-3 pt-3 border-t border-border">
|
||||||
<Info className="h-4 w-4 text-blue-400 shrink-0 mt-0.5" />
|
<Info className="h-3.5 w-3.5 text-blue-400 shrink-0 mt-0.5" />
|
||||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||||
These settings apply when you dismiss a warning from the Health Monitor.
|
These settings apply when you dismiss a warning from the Health Monitor.
|
||||||
Critical CPU temperature alerts always trigger regardless of settings to protect your hardware.
|
Critical CPU temperature alerts always trigger regardless of settings to protect your hardware.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -240,10 +240,15 @@ def save_health_settings():
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Retroactively sync all existing dismissed errors
|
||||||
|
# so changes are effective immediately, not just on next dismiss
|
||||||
|
synced_count = health_persistence.sync_dismissed_suppression()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'updated': updated,
|
'updated': updated,
|
||||||
'count': len(updated)
|
'count': len(updated),
|
||||||
|
'synced_dismissed': synced_count
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|||||||
@@ -752,12 +752,65 @@ class HealthPersistence:
|
|||||||
conn.close()
|
conn.close()
|
||||||
return {row[0]: row[1] for row in rows}
|
return {row[0]: row[1] for row in rows}
|
||||||
|
|
||||||
def get_suppression_categories(self) -> List[Dict[str, Any]]:
|
def sync_dismissed_suppression(self):
|
||||||
"""
|
"""
|
||||||
Get all health categories with their current suppression settings.
|
Retroactively update all existing dismissed errors to match current
|
||||||
Used by the settings page to render the per-category configuration.
|
user settings. Called when the user saves settings, so changes are
|
||||||
|
effective immediately on already-dismissed items.
|
||||||
|
|
||||||
|
For each dismissed error, looks up its category's configured hours
|
||||||
|
and updates the suppression_hours column to match.
|
||||||
"""
|
"""
|
||||||
category_labels = {
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Build reverse map: category -> setting_key
|
||||||
|
cat_to_setting = {v['category']: k
|
||||||
|
for k, v in self._get_category_labels().items()}
|
||||||
|
|
||||||
|
# Get all current suppression settings
|
||||||
|
current_settings = self.get_all_settings('suppress_')
|
||||||
|
|
||||||
|
# Get all dismissed (acknowledged) errors
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT id, error_key, category, suppression_hours
|
||||||
|
FROM errors WHERE acknowledged = 1
|
||||||
|
''')
|
||||||
|
dismissed = cursor.fetchall()
|
||||||
|
|
||||||
|
updated_count = 0
|
||||||
|
for err_id, error_key, category, old_hours in dismissed:
|
||||||
|
setting_key = None
|
||||||
|
for skey, meta in self._get_category_labels().items():
|
||||||
|
if meta['category'] == category:
|
||||||
|
setting_key = skey
|
||||||
|
break
|
||||||
|
|
||||||
|
if not setting_key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
stored = current_settings.get(setting_key)
|
||||||
|
new_hours = int(stored) if stored else self.DEFAULT_SUPPRESSION_HOURS
|
||||||
|
|
||||||
|
if new_hours != old_hours:
|
||||||
|
cursor.execute(
|
||||||
|
'UPDATE errors SET suppression_hours = ? WHERE id = ?',
|
||||||
|
(new_hours, err_id)
|
||||||
|
)
|
||||||
|
self._record_event(cursor, 'suppression_updated', error_key, {
|
||||||
|
'old_hours': old_hours,
|
||||||
|
'new_hours': new_hours,
|
||||||
|
'reason': 'settings_sync'
|
||||||
|
})
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return updated_count
|
||||||
|
|
||||||
|
def _get_category_labels(self) -> dict:
|
||||||
|
"""Internal helper for category label metadata."""
|
||||||
|
return {
|
||||||
'suppress_cpu': {'label': 'CPU Usage & Temperature', 'category': 'temperature', 'icon': 'cpu'},
|
'suppress_cpu': {'label': 'CPU Usage & Temperature', 'category': 'temperature', 'icon': 'cpu'},
|
||||||
'suppress_memory': {'label': 'Memory & Swap', 'category': 'memory', 'icon': 'memory'},
|
'suppress_memory': {'label': 'Memory & Swap', 'category': 'memory', 'icon': 'memory'},
|
||||||
'suppress_storage': {'label': 'Storage Mounts & Space', 'category': 'storage', 'icon': 'storage'},
|
'suppress_storage': {'label': 'Storage Mounts & Space', 'category': 'storage', 'icon': 'storage'},
|
||||||
@@ -769,7 +822,13 @@ class HealthPersistence:
|
|||||||
'suppress_updates': {'label': 'System Updates', 'category': 'updates', 'icon': 'updates'},
|
'suppress_updates': {'label': 'System Updates', 'category': 'updates', 'icon': 'updates'},
|
||||||
'suppress_security': {'label': 'Security & Certificates', 'category': 'security', 'icon': 'security'},
|
'suppress_security': {'label': 'Security & Certificates', 'category': 'security', 'icon': 'security'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_suppression_categories(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get all health categories with their current suppression settings.
|
||||||
|
Used by the settings page to render the per-category configuration.
|
||||||
|
"""
|
||||||
|
category_labels = self._get_category_labels()
|
||||||
current_settings = self.get_all_settings('suppress_')
|
current_settings = self.get_all_settings('suppress_')
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|||||||
Reference in New Issue
Block a user