update storage settings

This commit is contained in:
MacRimi
2026-03-19 19:07:26 +01:00
parent cefeac72fc
commit 876194cdc8
9 changed files with 668 additions and 63 deletions

View File

@@ -1426,13 +1426,34 @@ export function NotificationSettings() {
{config.ai_provider === "ollama" && (
<div className="space-y-2">
<Label className="text-xs sm:text-sm text-foreground/80">Ollama URL</Label>
<Input
className="h-9 text-sm font-mono"
placeholder="http://localhost:11434"
value={config.ai_ollama_url}
onChange={e => updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))}
disabled={!editMode}
/>
<div className="flex items-center gap-2">
<Input
className="h-9 text-sm font-mono flex-1"
placeholder="http://localhost:11434"
value={config.ai_ollama_url}
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>
)}
@@ -1495,38 +1516,28 @@ export function NotificationSettings() {
<div className="space-y-2">
<Label className="text-xs sm:text-sm text-foreground/80">Model</Label>
{config.ai_provider === "ollama" ? (
<div className="flex items-center gap-2">
<Select
value={config.ai_model || ""}
onValueChange={v => updateConfig(p => ({ ...p, ai_model: v }))}
disabled={!editMode || loadingOllamaModels}
>
<SelectTrigger className="h-9 text-sm font-mono flex-1">
<SelectValue placeholder={loadingOllamaModels ? "Loading models..." : "Select model"}>
{config.ai_model || (loadingOllamaModels ? "Loading..." : "Select model")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{ollamaModels.length > 0 ? (
ollamaModels.map(m => (
<SelectItem key={m} value={m} className="font-mono">{m}</SelectItem>
))
) : (
<SelectItem value="_none" disabled className="text-muted-foreground">
{loadingOllamaModels ? "Loading models..." : "No models found"}
</SelectItem>
)}
</SelectContent>
</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>
<Select
value={config.ai_model || ""}
onValueChange={v => updateConfig(p => ({ ...p, ai_model: v }))}
disabled={!editMode || loadingOllamaModels || ollamaModels.length === 0}
>
<SelectTrigger className="h-9 text-sm font-mono">
<SelectValue placeholder={ollamaModels.length === 0 ? "Click 'Load' to fetch models" : "Select model"}>
{config.ai_model || (ollamaModels.length === 0 ? "Click 'Load' to fetch models" : "Select model")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{ollamaModels.length > 0 ? (
ollamaModels.map(m => (
<SelectItem key={m} value={m} className="font-mono">{m}</SelectItem>
))
) : (
<SelectItem value="_none" disabled className="text-muted-foreground">
No models loaded - click Load button
</SelectItem>
)}
</SelectContent>
</Select>
) : (
<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"}

View File

@@ -2,9 +2,10 @@
import { useState, useEffect } from "react"
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Switch } from "./ui/switch"
import { Input } from "./ui/input"
import { Badge } from "./ui/badge"
import { getNetworkUnit } from "../lib/format-network"
@@ -47,6 +48,20 @@ interface ProxMenuxTool {
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() {
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
const [loadingTools, setLoadingTools] = useState(true)
@@ -61,11 +76,17 @@ export function Settings() {
const [savedAllHealth, setSavedAllHealth] = useState(false)
const [pendingChanges, setPendingChanges] = useState<Record<string, number>>({})
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(() => {
loadProxmenuxTools()
getUnitsSettings()
loadHealthSettings()
loadRemoteStorages()
}, [])
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 => {
if (hours === -1) return "-1"
const preset = SUPPRESSION_OPTIONS.find(o => o.value === String(hours))
@@ -439,6 +507,120 @@ export function Settings() {
</CardContent>
</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 */}
<NotificationSettings />

View File

@@ -674,11 +674,20 @@ export function StorageOverview() {
{proxmoxStorage.storage
.filter((storage) => storage && storage.name && storage.used >= 0 && storage.available >= 0)
.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
key={storage.name}
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">
@@ -687,27 +696,40 @@ export function StorageOverview() {
<Database className="h-5 w-5 text-muted-foreground" />
<h3 className="font-semibold text-lg">{storage.name}</h3>
<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 className="flex md:hidden items-center gap-2 flex-1">
<Database className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
<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>
{/* Desktop: Badge active + Porcentaje */}
<div className="hidden md:flex items-center gap-2">
<Badge
className={
storage.status === "active"
? "bg-green-500/10 text-green-500 border-green-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"
isExcluded
? "bg-purple-500/10 text-purple-400 border-purple-500/20"
: storage.status === "active"
? "bg-green-500/10 text-green-500 border-green-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>
<span className="text-sm font-medium">{storage.percent}%</span>
</div>
@@ -750,7 +772,8 @@ export function StorageOverview() {
</div>
</div>
</div>
))}
)
})}
</div>
</CardContent>
</Card>