mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-02 12:26:23 +00:00
update storage settings
This commit is contained in:
@@ -1426,13 +1426,34 @@ export function NotificationSettings() {
|
|||||||
{config.ai_provider === "ollama" && (
|
{config.ai_provider === "ollama" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs sm:text-sm text-foreground/80">Ollama URL</Label>
|
<Label className="text-xs sm:text-sm text-foreground/80">Ollama URL</Label>
|
||||||
<Input
|
<div className="flex items-center gap-2">
|
||||||
className="h-9 text-sm font-mono"
|
<Input
|
||||||
placeholder="http://localhost:11434"
|
className="h-9 text-sm font-mono flex-1"
|
||||||
value={config.ai_ollama_url}
|
placeholder="http://localhost:11434"
|
||||||
onChange={e => updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))}
|
value={config.ai_ollama_url}
|
||||||
disabled={!editMode}
|
onChange={e => updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))}
|
||||||
/>
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 px-3 shrink-0"
|
||||||
|
onClick={() => fetchOllamaModels(config.ai_ollama_url)}
|
||||||
|
disabled={!editMode || loadingOllamaModels || !config.ai_ollama_url}
|
||||||
|
>
|
||||||
|
{loadingOllamaModels ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-1" />
|
||||||
|
Load
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{ollamaModels.length > 0 && (
|
||||||
|
<p className="text-xs text-green-500">{ollamaModels.length} models found</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1495,38 +1516,28 @@ export function NotificationSettings() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs sm:text-sm text-foreground/80">Model</Label>
|
<Label className="text-xs sm:text-sm text-foreground/80">Model</Label>
|
||||||
{config.ai_provider === "ollama" ? (
|
{config.ai_provider === "ollama" ? (
|
||||||
<div className="flex items-center gap-2">
|
<Select
|
||||||
<Select
|
value={config.ai_model || ""}
|
||||||
value={config.ai_model || ""}
|
onValueChange={v => updateConfig(p => ({ ...p, ai_model: v }))}
|
||||||
onValueChange={v => updateConfig(p => ({ ...p, ai_model: v }))}
|
disabled={!editMode || loadingOllamaModels || ollamaModels.length === 0}
|
||||||
disabled={!editMode || loadingOllamaModels}
|
>
|
||||||
>
|
<SelectTrigger className="h-9 text-sm font-mono">
|
||||||
<SelectTrigger className="h-9 text-sm font-mono flex-1">
|
<SelectValue placeholder={ollamaModels.length === 0 ? "Click 'Load' to fetch models" : "Select model"}>
|
||||||
<SelectValue placeholder={loadingOllamaModels ? "Loading models..." : "Select model"}>
|
{config.ai_model || (ollamaModels.length === 0 ? "Click 'Load' to fetch models" : "Select model")}
|
||||||
{config.ai_model || (loadingOllamaModels ? "Loading..." : "Select model")}
|
</SelectValue>
|
||||||
</SelectValue>
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
{ollamaModels.length > 0 ? (
|
||||||
{ollamaModels.length > 0 ? (
|
ollamaModels.map(m => (
|
||||||
ollamaModels.map(m => (
|
<SelectItem key={m} value={m} className="font-mono">{m}</SelectItem>
|
||||||
<SelectItem key={m} value={m} className="font-mono">{m}</SelectItem>
|
))
|
||||||
))
|
) : (
|
||||||
) : (
|
<SelectItem value="_none" disabled className="text-muted-foreground">
|
||||||
<SelectItem value="_none" disabled className="text-muted-foreground">
|
No models loaded - click Load button
|
||||||
{loadingOllamaModels ? "Loading models..." : "No models found"}
|
</SelectItem>
|
||||||
</SelectItem>
|
)}
|
||||||
)}
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
|
||||||
<button
|
|
||||||
onClick={() => fetchOllamaModels(config.ai_ollama_url)}
|
|
||||||
disabled={!editMode || loadingOllamaModels}
|
|
||||||
className="h-9 w-9 flex items-center justify-center rounded-md border border-border hover:bg-muted transition-colors shrink-0 disabled:opacity-50"
|
|
||||||
title="Refresh models"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`h-4 w-4 ${loadingOllamaModels ? 'animate-spin' : ''}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="h-9 px-3 flex items-center rounded-md border border-border bg-muted/50 text-sm font-mono text-muted-foreground">
|
<div className="h-9 px-3 flex items-center rounded-md border border-border bg-muted/50 text-sm font-mono text-muted-foreground">
|
||||||
{AI_PROVIDERS.find(p => p.value === config.ai_provider)?.model || "default"}
|
{AI_PROVIDERS.find(p => p.value === config.ai_provider)?.model || "default"}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||||
import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check } from "lucide-react"
|
import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff } from "lucide-react"
|
||||||
import { NotificationSettings } from "./notification-settings"
|
import { NotificationSettings } from "./notification-settings"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
|
import { Switch } from "./ui/switch"
|
||||||
import { Input } from "./ui/input"
|
import { Input } from "./ui/input"
|
||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { getNetworkUnit } from "../lib/format-network"
|
import { getNetworkUnit } from "../lib/format-network"
|
||||||
@@ -47,6 +48,20 @@ interface ProxMenuxTool {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RemoteStorage {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
status: string
|
||||||
|
total: number
|
||||||
|
used: number
|
||||||
|
available: number
|
||||||
|
percent: number
|
||||||
|
exclude_health: boolean
|
||||||
|
exclude_notifications: boolean
|
||||||
|
excluded_at?: string
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
|
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
|
||||||
const [loadingTools, setLoadingTools] = useState(true)
|
const [loadingTools, setLoadingTools] = useState(true)
|
||||||
@@ -61,11 +76,17 @@ export function Settings() {
|
|||||||
const [savedAllHealth, setSavedAllHealth] = useState(false)
|
const [savedAllHealth, setSavedAllHealth] = useState(false)
|
||||||
const [pendingChanges, setPendingChanges] = useState<Record<string, number>>({})
|
const [pendingChanges, setPendingChanges] = useState<Record<string, number>>({})
|
||||||
const [customValues, setCustomValues] = useState<Record<string, string>>({})
|
const [customValues, setCustomValues] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
|
// Remote Storage Exclusions
|
||||||
|
const [remoteStorages, setRemoteStorages] = useState<RemoteStorage[]>([])
|
||||||
|
const [loadingStorages, setLoadingStorages] = useState(true)
|
||||||
|
const [savingStorage, setSavingStorage] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProxmenuxTools()
|
loadProxmenuxTools()
|
||||||
getUnitsSettings()
|
getUnitsSettings()
|
||||||
loadHealthSettings()
|
loadHealthSettings()
|
||||||
|
loadRemoteStorages()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadProxmenuxTools = async () => {
|
const loadProxmenuxTools = async () => {
|
||||||
@@ -114,6 +135,53 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadRemoteStorages = async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchApi("/api/health/remote-storages")
|
||||||
|
if (data.storages) {
|
||||||
|
setRemoteStorages(data.storages)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load remote storages:", err)
|
||||||
|
} finally {
|
||||||
|
setLoadingStorages(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStorageExclusionChange = async (storageName: string, storageType: string, excludeHealth: boolean, excludeNotifications: boolean) => {
|
||||||
|
setSavingStorage(storageName)
|
||||||
|
try {
|
||||||
|
// If both are false, remove the exclusion
|
||||||
|
if (!excludeHealth && !excludeNotifications) {
|
||||||
|
await fetchApi(`/api/health/storage-exclusions/${encodeURIComponent(storageName)}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await fetchApi("/api/health/storage-exclusions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
storage_name: storageName,
|
||||||
|
storage_type: storageType,
|
||||||
|
exclude_health: excludeHealth,
|
||||||
|
exclude_notifications: excludeNotifications
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
setRemoteStorages(prev => prev.map(s =>
|
||||||
|
s.name === storageName
|
||||||
|
? { ...s, exclude_health: excludeHealth, exclude_notifications: excludeNotifications }
|
||||||
|
: s
|
||||||
|
))
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update storage exclusion:", err)
|
||||||
|
} finally {
|
||||||
|
setSavingStorage(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getSelectValue = (hours: number, key: string): string => {
|
const getSelectValue = (hours: number, key: string): string => {
|
||||||
if (hours === -1) return "-1"
|
if (hours === -1) return "-1"
|
||||||
const preset = SUPPRESSION_OPTIONS.find(o => o.value === String(hours))
|
const preset = SUPPRESSION_OPTIONS.find(o => o.value === String(hours))
|
||||||
@@ -439,6 +507,120 @@ export function Settings() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Remote Storage Exclusions */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5 text-purple-500" />
|
||||||
|
<CardTitle>Remote Storage Exclusions</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Exclude remote storages (PBS, NFS, CIFS, etc.) from health monitoring and notifications.
|
||||||
|
Use this for storages that are intentionally offline or have limited API access.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loadingStorages ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="animate-spin h-8 w-8 border-4 border-purple-500 border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
) : remoteStorages.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<CloudOff className="h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-muted-foreground">No remote storages detected</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
PBS, NFS, CIFS, and other remote storages will appear here when configured
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-0">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="grid grid-cols-[1fr_auto_auto] gap-4 pb-2 mb-1 border-b border-border">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">Storage</span>
|
||||||
|
<span className="text-xs font-medium text-muted-foreground text-center w-20">Health</span>
|
||||||
|
<span className="text-xs font-medium text-muted-foreground text-center w-20">Alerts</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Storage rows */}
|
||||||
|
<div className="divide-y divide-border/50">
|
||||||
|
{remoteStorages.map((storage) => {
|
||||||
|
const isExcluded = storage.exclude_health || storage.exclude_notifications
|
||||||
|
const isSaving = savingStorage === storage.name
|
||||||
|
const isOffline = storage.status === 'error' || storage.total === 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={storage.name} className="grid grid-cols-[1fr_auto_auto] gap-4 py-3 items-center">
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<div className={`w-2 h-2 rounded-full shrink-0 ${
|
||||||
|
isOffline ? 'bg-red-500' : 'bg-green-500'
|
||||||
|
}`} />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium truncate">{storage.name}</span>
|
||||||
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0 shrink-0">
|
||||||
|
{storage.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{isOffline && (
|
||||||
|
<p className="text-[11px] text-red-400 mt-0.5">Offline or unavailable</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center w-20">
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<Switch
|
||||||
|
checked={!storage.exclude_health}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleStorageExclusionChange(
|
||||||
|
storage.name,
|
||||||
|
storage.type,
|
||||||
|
!checked,
|
||||||
|
storage.exclude_notifications
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center w-20">
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<Switch
|
||||||
|
checked={!storage.exclude_notifications}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleStorageExclusionChange(
|
||||||
|
storage.name,
|
||||||
|
storage.type,
|
||||||
|
storage.exclude_health,
|
||||||
|
!checked
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info footer */}
|
||||||
|
<div className="flex items-start gap-2 mt-3 pt-3 border-t border-border">
|
||||||
|
<Info className="h-3.5 w-3.5 text-purple-400 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||||
|
<strong>Health:</strong> When OFF, the storage won't trigger warnings/critical alerts in the Health Monitor.
|
||||||
|
<br />
|
||||||
|
<strong>Alerts:</strong> When OFF, no notifications will be sent for this storage.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Notification Settings */}
|
{/* Notification Settings */}
|
||||||
<NotificationSettings />
|
<NotificationSettings />
|
||||||
|
|
||||||
|
|||||||
@@ -674,11 +674,20 @@ export function StorageOverview() {
|
|||||||
{proxmoxStorage.storage
|
{proxmoxStorage.storage
|
||||||
.filter((storage) => storage && storage.name && storage.used >= 0 && storage.available >= 0)
|
.filter((storage) => storage && storage.name && storage.used >= 0 && storage.available >= 0)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((storage) => (
|
.map((storage) => {
|
||||||
|
// Check if storage is excluded from monitoring
|
||||||
|
const isExcluded = storage.excluded === true
|
||||||
|
const hasError = storage.status === "error" && !isExcluded
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={storage.name}
|
key={storage.name}
|
||||||
className={`border rounded-lg p-4 ${
|
className={`border rounded-lg p-4 ${
|
||||||
storage.status === "error" ? "border-red-500/50 bg-red-500/5" : ""
|
hasError
|
||||||
|
? "border-red-500/50 bg-red-500/5"
|
||||||
|
: isExcluded
|
||||||
|
? "border-purple-500/30 bg-purple-500/5 opacity-75"
|
||||||
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@@ -687,27 +696,40 @@ export function StorageOverview() {
|
|||||||
<Database className="h-5 w-5 text-muted-foreground" />
|
<Database className="h-5 w-5 text-muted-foreground" />
|
||||||
<h3 className="font-semibold text-lg">{storage.name}</h3>
|
<h3 className="font-semibold text-lg">{storage.name}</h3>
|
||||||
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
|
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
|
||||||
|
{isExcluded && (
|
||||||
|
<Badge className="bg-purple-500/10 text-purple-400 border-purple-500/20 text-[10px]">
|
||||||
|
excluded
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex md:hidden items-center gap-2 flex-1">
|
<div className="flex md:hidden items-center gap-2 flex-1">
|
||||||
<Database className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
<Database className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||||
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
|
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
|
||||||
<h3 className="font-semibold text-base flex-1 min-w-0 truncate">{storage.name}</h3>
|
<h3 className="font-semibold text-base flex-1 min-w-0 truncate">{storage.name}</h3>
|
||||||
{getStatusIcon(storage.status)}
|
{isExcluded ? (
|
||||||
|
<Badge className="bg-purple-500/10 text-purple-400 border-purple-500/20 text-[10px]">
|
||||||
|
excluded
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
getStatusIcon(storage.status)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop: Badge active + Porcentaje */}
|
{/* Desktop: Badge active + Porcentaje */}
|
||||||
<div className="hidden md:flex items-center gap-2">
|
<div className="hidden md:flex items-center gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className={
|
className={
|
||||||
storage.status === "active"
|
isExcluded
|
||||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
? "bg-purple-500/10 text-purple-400 border-purple-500/20"
|
||||||
: storage.status === "error"
|
: storage.status === "active"
|
||||||
? "bg-red-500/10 text-red-500 border-red-500/20"
|
? "bg-green-500/10 text-green-500 border-green-500/20"
|
||||||
: "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
: storage.status === "error"
|
||||||
|
? "bg-red-500/10 text-red-500 border-red-500/20"
|
||||||
|
: "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{storage.status}
|
{isExcluded ? "not monitored" : storage.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-sm font-medium">{storage.percent}%</span>
|
<span className="text-sm font-medium">{storage.percent}%</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -750,7 +772,8 @@ export function StorageOverview() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -326,3 +326,133 @@ def save_health_settings():
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ── Remote Storage Exclusions Endpoints ──
|
||||||
|
|
||||||
|
@health_bp.route('/api/health/remote-storages', methods=['GET'])
|
||||||
|
def get_remote_storages():
|
||||||
|
"""
|
||||||
|
Get list of all remote storages with their exclusion status.
|
||||||
|
Remote storages are those that can be offline (PBS, NFS, CIFS, etc.)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from proxmox_storage_monitor import proxmox_storage_monitor
|
||||||
|
|
||||||
|
# Get current storage status
|
||||||
|
storage_status = proxmox_storage_monitor.get_storage_status()
|
||||||
|
all_storages = storage_status.get('available', []) + storage_status.get('unavailable', [])
|
||||||
|
|
||||||
|
# Filter to only remote types
|
||||||
|
remote_types = health_persistence.REMOTE_STORAGE_TYPES
|
||||||
|
remote_storages = [s for s in all_storages if s.get('type', '').lower() in remote_types]
|
||||||
|
|
||||||
|
# Get current exclusions
|
||||||
|
exclusions = {e['storage_name']: e for e in health_persistence.get_excluded_storages()}
|
||||||
|
|
||||||
|
# Combine info
|
||||||
|
result = []
|
||||||
|
for storage in remote_storages:
|
||||||
|
name = storage.get('name', '')
|
||||||
|
exclusion = exclusions.get(name, {})
|
||||||
|
result.append({
|
||||||
|
'name': name,
|
||||||
|
'type': storage.get('type', 'unknown'),
|
||||||
|
'status': storage.get('status', 'unknown'),
|
||||||
|
'total': storage.get('total', 0),
|
||||||
|
'used': storage.get('used', 0),
|
||||||
|
'available': storage.get('available', 0),
|
||||||
|
'percent': storage.get('percent', 0),
|
||||||
|
'exclude_health': exclusion.get('exclude_health', 0) == 1,
|
||||||
|
'exclude_notifications': exclusion.get('exclude_notifications', 0) == 1,
|
||||||
|
'excluded_at': exclusion.get('excluded_at'),
|
||||||
|
'reason': exclusion.get('reason')
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'storages': result,
|
||||||
|
'remote_types': list(remote_types)
|
||||||
|
})
|
||||||
|
except ImportError:
|
||||||
|
return jsonify({'error': 'Storage monitor not available', 'storages': []}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@health_bp.route('/api/health/storage-exclusions', methods=['GET'])
|
||||||
|
def get_storage_exclusions():
|
||||||
|
"""Get all storage exclusions."""
|
||||||
|
try:
|
||||||
|
exclusions = health_persistence.get_excluded_storages()
|
||||||
|
return jsonify({'exclusions': exclusions})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@health_bp.route('/api/health/storage-exclusions', methods=['POST'])
|
||||||
|
def save_storage_exclusion():
|
||||||
|
"""
|
||||||
|
Add or update a storage exclusion.
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"storage_name": "pbs-backup",
|
||||||
|
"storage_type": "pbs",
|
||||||
|
"exclude_health": true,
|
||||||
|
"exclude_notifications": true,
|
||||||
|
"reason": "PBS server is offline daily"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or 'storage_name' not in data:
|
||||||
|
return jsonify({'error': 'storage_name is required'}), 400
|
||||||
|
|
||||||
|
storage_name = data['storage_name']
|
||||||
|
storage_type = data.get('storage_type', 'unknown')
|
||||||
|
exclude_health = data.get('exclude_health', True)
|
||||||
|
exclude_notifications = data.get('exclude_notifications', True)
|
||||||
|
reason = data.get('reason')
|
||||||
|
|
||||||
|
# Check if already excluded
|
||||||
|
existing = health_persistence.get_excluded_storages()
|
||||||
|
exists = any(e['storage_name'] == storage_name for e in existing)
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
# Update existing
|
||||||
|
success = health_persistence.update_storage_exclusion(
|
||||||
|
storage_name, exclude_health, exclude_notifications
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Add new
|
||||||
|
success = health_persistence.exclude_storage(
|
||||||
|
storage_name, storage_type, exclude_health, exclude_notifications, reason
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Storage {storage_name} exclusion saved',
|
||||||
|
'storage_name': storage_name
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Failed to save exclusion'}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@health_bp.route('/api/health/storage-exclusions/<storage_name>', methods=['DELETE'])
|
||||||
|
def delete_storage_exclusion(storage_name):
|
||||||
|
"""Remove a storage from the exclusion list."""
|
||||||
|
try:
|
||||||
|
success = health_persistence.remove_storage_exclusion(storage_name)
|
||||||
|
if success:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Storage {storage_name} removed from exclusions'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Storage not found in exclusions'}), 404
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|||||||
@@ -130,9 +130,10 @@ def get_ollama_models():
|
|||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
result = json.loads(resp.read().decode('utf-8'))
|
result = json.loads(resp.read().decode('utf-8'))
|
||||||
models = [m.get('name', '').split(':')[0] for m in result.get('models', [])]
|
# Keep full model names (including tags like :latest, :3b-instruct-q4_0)
|
||||||
# Remove duplicates and sort
|
models = [m.get('name', '') for m in result.get('models', []) if m.get('name')]
|
||||||
models = sorted(list(set(models)))
|
# Sort alphabetically
|
||||||
|
models = sorted(models)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'models': models,
|
'models': models,
|
||||||
|
|||||||
@@ -2639,6 +2639,24 @@ def get_proxmox_storage():
|
|||||||
for unavailable_storage in unavailable_storages:
|
for unavailable_storage in unavailable_storages:
|
||||||
if unavailable_storage['name'] not in existing_storage_names:
|
if unavailable_storage['name'] not in existing_storage_names:
|
||||||
storage_list.append(unavailable_storage)
|
storage_list.append(unavailable_storage)
|
||||||
|
|
||||||
|
# Get storage exclusions to mark excluded storages
|
||||||
|
try:
|
||||||
|
excluded_health = health_persistence.get_excluded_storage_names('health')
|
||||||
|
remote_types = health_persistence.REMOTE_STORAGE_TYPES
|
||||||
|
|
||||||
|
for storage in storage_list:
|
||||||
|
storage_name = storage.get('name', '')
|
||||||
|
storage_type = storage.get('type', '').lower()
|
||||||
|
|
||||||
|
# Mark if this is a remote storage type
|
||||||
|
storage['is_remote'] = storage_type in remote_types
|
||||||
|
|
||||||
|
# Mark if excluded from health monitoring
|
||||||
|
storage['excluded'] = storage_name in excluded_health
|
||||||
|
except Exception:
|
||||||
|
# If exclusion check fails, continue without it
|
||||||
|
pass
|
||||||
|
|
||||||
return {'storage': storage_list}
|
return {'storage': storage_list}
|
||||||
|
|
||||||
|
|||||||
@@ -4431,6 +4431,8 @@ class HealthMonitor:
|
|||||||
Detects unavailable storages configured in PVE.
|
Detects unavailable storages configured in PVE.
|
||||||
Returns CRITICAL if any configured storage is unavailable.
|
Returns CRITICAL if any configured storage is unavailable.
|
||||||
Returns None if the module is not available.
|
Returns None if the module is not available.
|
||||||
|
|
||||||
|
Respects storage exclusions: excluded storages are reported as INFO, not CRITICAL.
|
||||||
"""
|
"""
|
||||||
if not PROXMOX_STORAGE_AVAILABLE:
|
if not PROXMOX_STORAGE_AVAILABLE:
|
||||||
return None
|
return None
|
||||||
@@ -4443,12 +4445,22 @@ class HealthMonitor:
|
|||||||
storage_status = proxmox_storage_monitor.get_storage_status()
|
storage_status = proxmox_storage_monitor.get_storage_status()
|
||||||
unavailable_storages = storage_status.get('unavailable', [])
|
unavailable_storages = storage_status.get('unavailable', [])
|
||||||
|
|
||||||
if not unavailable_storages:
|
# Get excluded storage names for health monitoring
|
||||||
# All storages are available. We should also clear any previously recorded storage errors.
|
excluded_names = health_persistence.get_excluded_storage_names('health')
|
||||||
|
|
||||||
|
# Separate excluded storages from real issues
|
||||||
|
excluded_unavailable = [s for s in unavailable_storages if s.get('name', '') in excluded_names]
|
||||||
|
real_unavailable = [s for s in unavailable_storages if s.get('name', '') not in excluded_names]
|
||||||
|
|
||||||
|
if not real_unavailable:
|
||||||
|
# All non-excluded storages are available. Clear any previously recorded storage errors.
|
||||||
active_errors = health_persistence.get_active_errors()
|
active_errors = health_persistence.get_active_errors()
|
||||||
for error in active_errors:
|
for error in active_errors:
|
||||||
if error.get('category') == 'storage' and error.get('error_key', '').startswith('storage_unavailable_'):
|
if error.get('category') == 'storage' and error.get('error_key', '').startswith('storage_unavailable_'):
|
||||||
health_persistence.clear_error(error['error_key'])
|
# Only clear if not an excluded storage
|
||||||
|
storage_name = error.get('error_key', '').replace('storage_unavailable_', '')
|
||||||
|
if storage_name not in excluded_names:
|
||||||
|
health_persistence.clear_error(error['error_key'])
|
||||||
|
|
||||||
# Build checks from all configured storages for descriptive display
|
# Build checks from all configured storages for descriptive display
|
||||||
available_storages = storage_status.get('available', [])
|
available_storages = storage_status.get('available', [])
|
||||||
@@ -4460,12 +4472,24 @@ class HealthMonitor:
|
|||||||
'status': 'OK',
|
'status': 'OK',
|
||||||
'detail': f'{st_type} storage available'
|
'detail': f'{st_type} storage available'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add excluded unavailable storages as INFO (not CRITICAL)
|
||||||
|
for st in excluded_unavailable:
|
||||||
|
st_name = st.get('name', 'unknown')
|
||||||
|
st_type = st.get('type', 'unknown')
|
||||||
|
checks[st_name] = {
|
||||||
|
'status': 'INFO',
|
||||||
|
'detail': f'{st_type} storage excluded from monitoring',
|
||||||
|
'excluded': True
|
||||||
|
}
|
||||||
|
|
||||||
if not checks:
|
if not checks:
|
||||||
checks['proxmox_storages'] = {'status': 'OK', 'detail': 'All storages available'}
|
checks['proxmox_storages'] = {'status': 'OK', 'detail': 'All storages available'}
|
||||||
return {'status': 'OK', 'checks': checks}
|
return {'status': 'OK', 'checks': checks}
|
||||||
|
|
||||||
storage_details = {}
|
storage_details = {}
|
||||||
for storage in unavailable_storages:
|
# Only process non-excluded unavailable storages as errors
|
||||||
|
for storage in real_unavailable:
|
||||||
storage_name = storage['name']
|
storage_name = storage['name']
|
||||||
error_key = f'storage_unavailable_{storage_name}'
|
error_key = f'storage_unavailable_{storage_name}'
|
||||||
status_detail = storage.get('status_detail', 'unavailable')
|
status_detail = storage.get('status_detail', 'unavailable')
|
||||||
@@ -4508,6 +4532,17 @@ class HealthMonitor:
|
|||||||
'detail': st_info.get('reason', 'Unavailable'),
|
'detail': st_info.get('reason', 'Unavailable'),
|
||||||
'dismissable': False
|
'dismissable': False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add excluded unavailable storages as INFO (not as errors)
|
||||||
|
for st in excluded_unavailable:
|
||||||
|
st_name = st.get('name', 'unknown')
|
||||||
|
st_type = st.get('type', 'unknown')
|
||||||
|
checks[st_name] = {
|
||||||
|
'status': 'INFO',
|
||||||
|
'detail': f'{st_type} storage excluded from monitoring (offline)',
|
||||||
|
'excluded': True
|
||||||
|
}
|
||||||
|
|
||||||
# Also add available storages
|
# Also add available storages
|
||||||
available_list = storage_status.get('available', [])
|
available_list = storage_status.get('available', [])
|
||||||
unavail_names = {s['name'] for s in unavailable_storages}
|
unavail_names = {s['name'] for s in unavailable_storages}
|
||||||
@@ -4518,12 +4553,21 @@ class HealthMonitor:
|
|||||||
'detail': f'{st.get("type", "unknown")} storage available'
|
'detail': f'{st.get("type", "unknown")} storage available'
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
# Determine overall status based on non-excluded issues only
|
||||||
'status': 'CRITICAL',
|
if real_unavailable:
|
||||||
'reason': f'{len(unavailable_storages)} Proxmox storage(s) unavailable',
|
return {
|
||||||
'details': storage_details,
|
'status': 'CRITICAL',
|
||||||
'checks': checks
|
'reason': f'{len(real_unavailable)} Proxmox storage(s) unavailable',
|
||||||
}
|
'details': storage_details,
|
||||||
|
'checks': checks
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Only excluded storages are unavailable - this is OK
|
||||||
|
return {
|
||||||
|
'status': 'OK',
|
||||||
|
'reason': 'All monitored storages available',
|
||||||
|
'checks': checks
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[HealthMonitor] Error checking Proxmox storage: {e}")
|
print(f"[HealthMonitor] Error checking Proxmox storage: {e}")
|
||||||
|
|||||||
@@ -235,6 +235,22 @@ class HealthPersistence:
|
|||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_obs_disk ON disk_observations(disk_registry_id)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_obs_disk ON disk_observations(disk_registry_id)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_obs_dismissed ON disk_observations(dismissed)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_obs_dismissed ON disk_observations(dismissed)')
|
||||||
|
|
||||||
|
# ── Remote Storage Exclusions System ──
|
||||||
|
# Allows users to permanently exclude remote storages (PBS, NFS, CIFS, etc.)
|
||||||
|
# from health monitoring and notifications
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS excluded_storages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
storage_name TEXT UNIQUE NOT NULL,
|
||||||
|
storage_type TEXT NOT NULL,
|
||||||
|
excluded_at TEXT NOT NULL,
|
||||||
|
exclude_health INTEGER DEFAULT 1,
|
||||||
|
exclude_notifications INTEGER DEFAULT 1,
|
||||||
|
reason TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_excluded_storage ON excluded_storages(storage_name)')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -1845,5 +1861,172 @@ class HealthPersistence:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# ── Remote Storage Exclusions Methods ──
|
||||||
|
|
||||||
|
# Types considered "remote" and eligible for exclusion
|
||||||
|
REMOTE_STORAGE_TYPES = {'pbs', 'nfs', 'cifs', 'glusterfs', 'iscsi', 'iscsidirect', 'cephfs', 'rbd'}
|
||||||
|
|
||||||
|
def is_remote_storage_type(self, storage_type: str) -> bool:
|
||||||
|
"""Check if a storage type is considered remote/external."""
|
||||||
|
return storage_type.lower() in self.REMOTE_STORAGE_TYPES
|
||||||
|
|
||||||
|
def get_excluded_storages(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Get list of all excluded remote storages."""
|
||||||
|
try:
|
||||||
|
with self._db_connection(row_factory=True) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT storage_name, storage_type, excluded_at,
|
||||||
|
exclude_health, exclude_notifications, reason
|
||||||
|
FROM excluded_storages
|
||||||
|
''')
|
||||||
|
return [dict(row) for row in cursor.fetchall()]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[HealthPersistence] Error getting excluded storages: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def is_storage_excluded(self, storage_name: str, check_type: str = 'health') -> bool:
|
||||||
|
"""
|
||||||
|
Check if a storage is excluded from monitoring.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_name: Name of the storage
|
||||||
|
check_type: 'health' or 'notifications'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if storage is excluded for the given check type
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with self._db_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
column = 'exclude_health' if check_type == 'health' else 'exclude_notifications'
|
||||||
|
cursor.execute(f'''
|
||||||
|
SELECT {column} FROM excluded_storages
|
||||||
|
WHERE storage_name = ?
|
||||||
|
''', (storage_name,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return row is not None and row[0] == 1
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def exclude_storage(self, storage_name: str, storage_type: str,
|
||||||
|
exclude_health: bool = True, exclude_notifications: bool = True,
|
||||||
|
reason: str = None) -> bool:
|
||||||
|
"""
|
||||||
|
Add a storage to the exclusion list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_name: Name of the storage to exclude
|
||||||
|
storage_type: Type of storage (pbs, nfs, etc.)
|
||||||
|
exclude_health: Whether to exclude from health monitoring
|
||||||
|
exclude_notifications: Whether to exclude from notifications
|
||||||
|
reason: Optional reason for exclusion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successfully excluded
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
with self._db_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO excluded_storages
|
||||||
|
(storage_name, storage_type, excluded_at, exclude_health, exclude_notifications, reason)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(storage_name) DO UPDATE SET
|
||||||
|
exclude_health = excluded.exclude_health,
|
||||||
|
exclude_notifications = excluded.exclude_notifications,
|
||||||
|
reason = excluded.reason
|
||||||
|
''', (storage_name, storage_type, now,
|
||||||
|
1 if exclude_health else 0,
|
||||||
|
1 if exclude_notifications else 0,
|
||||||
|
reason))
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[HealthPersistence] Error excluding storage: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_storage_exclusion(self, storage_name: str,
|
||||||
|
exclude_health: Optional[bool] = None,
|
||||||
|
exclude_notifications: Optional[bool] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Update exclusion settings for a storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_name: Name of the storage
|
||||||
|
exclude_health: New value for health exclusion (None = don't change)
|
||||||
|
exclude_notifications: New value for notifications exclusion (None = don't change)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successfully updated
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with self._db_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
updates = []
|
||||||
|
values = []
|
||||||
|
|
||||||
|
if exclude_health is not None:
|
||||||
|
updates.append('exclude_health = ?')
|
||||||
|
values.append(1 if exclude_health else 0)
|
||||||
|
|
||||||
|
if exclude_notifications is not None:
|
||||||
|
updates.append('exclude_notifications = ?')
|
||||||
|
values.append(1 if exclude_notifications else 0)
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
return True
|
||||||
|
|
||||||
|
values.append(storage_name)
|
||||||
|
cursor.execute(f'''
|
||||||
|
UPDATE excluded_storages
|
||||||
|
SET {', '.join(updates)}
|
||||||
|
WHERE storage_name = ?
|
||||||
|
''', values)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[HealthPersistence] Error updating storage exclusion: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_storage_exclusion(self, storage_name: str) -> bool:
|
||||||
|
"""Remove a storage from the exclusion list."""
|
||||||
|
try:
|
||||||
|
with self._db_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM excluded_storages WHERE storage_name = ?
|
||||||
|
''', (storage_name,))
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[HealthPersistence] Error removing storage exclusion: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_excluded_storage_names(self, check_type: str = 'health') -> set:
|
||||||
|
"""
|
||||||
|
Get set of storage names excluded for a specific check type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
check_type: 'health' or 'notifications'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of excluded storage names
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with self._db_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
column = 'exclude_health' if check_type == 'health' else 'exclude_notifications'
|
||||||
|
cursor.execute(f'''
|
||||||
|
SELECT storage_name FROM excluded_storages
|
||||||
|
WHERE {column} = 1
|
||||||
|
''')
|
||||||
|
return {row[0] for row in cursor.fetchall()}
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
# Global instance
|
# Global instance
|
||||||
health_persistence = HealthPersistence()
|
health_persistence = HealthPersistence()
|
||||||
|
|||||||
@@ -648,6 +648,19 @@ class NotificationManager:
|
|||||||
if self._is_backup_running():
|
if self._is_backup_running():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check storage exclusions for storage-related events.
|
||||||
|
# If the storage is excluded from notifications, suppress the event entirely.
|
||||||
|
_STORAGE_EVENTS = {'storage_unavailable', 'storage_low_space', 'storage_warning', 'storage_error'}
|
||||||
|
if event.event_type in _STORAGE_EVENTS:
|
||||||
|
storage_name = event.data.get('storage_name') or event.data.get('name')
|
||||||
|
if storage_name:
|
||||||
|
try:
|
||||||
|
from health_persistence import health_persistence
|
||||||
|
if health_persistence.is_storage_excluded(storage_name, 'notifications'):
|
||||||
|
return # Storage is excluded from notifications, skip silently
|
||||||
|
except Exception:
|
||||||
|
pass # Continue if check fails
|
||||||
|
|
||||||
# Check cooldown
|
# Check cooldown
|
||||||
if not self._check_cooldown(event):
|
if not self._check_cooldown(event):
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user