diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index 1bf5b78c..a03b5685 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -11,7 +11,7 @@ import { Badge } from "./ui/badge" import { Progress } from "./ui/progress" import { Button } from "./ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "./ui/dialog" -import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp, ChevronRight, Terminal, Archive, Plus, PlusCircle, Loader2, Database, Shield, Bell, FileText, Settings2, Activity, Package, RefreshCw, EthernetPort, ArrowUpCircle, Info, CheckCircle2, EyeOff, Eye, Trash2, Check, X, AlertTriangle, AlertCircle, Search, Pin, Tag as TagIcon } from 'lucide-react' +import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp, ChevronRight, Terminal, Archive, Plus, PlusCircle, Loader2, Database, Shield, FileText, Settings2, Activity, Package, RefreshCw, EthernetPort, ArrowUpCircle, Info, CheckCircle2, EyeOff, Eye, Trash2, Check, X, AlertTriangle, AlertCircle, Search, Pin, Tag as TagIcon } from 'lucide-react' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Checkbox } from "./ui/checkbox" import { Switch } from "./ui/switch" @@ -963,7 +963,6 @@ export function VirtualMachines() { const [showBackupModal, setShowBackupModal] = useState(false) const [backupMode, setBackupMode] = useState("snapshot") const [backupProtected, setBackupProtected] = useState(false) - const [backupNotification, setBackupNotification] = useState("auto") const [backupNotes, setBackupNotes] = useState("{{guestname}}") const [backupPbsChangeMode, setBackupPbsChangeMode] = useState("default") @@ -1698,7 +1697,6 @@ export function VirtualMachines() { // Reset modal to defaults setBackupMode("snapshot") setBackupProtected(false) - setBackupNotification("auto") setBackupNotes("{{guestname}}") setBackupPbsChangeMode("default") // Auto-select first storage if none selected @@ -1722,7 +1720,6 @@ export function VirtualMachines() { mode: backupMode, compress: "zstd", protected: backupProtected, - notification: backupNotification, notes: backupNotes, pbs_change_detection: backupPbsChangeMode }), @@ -1879,40 +1876,6 @@ export function VirtualMachines() { setTerminalOpen(true) } -const handleDownloadLogs = async (vmid: number, vmName: string) => { - try { - const data = await fetchApi(`/api/vms/${vmid}/logs`) - - // Format logs as plain text - let logText = `=== ${t("vmLxc.logs.header", { name: vmName, vmid })} ===\n` - logText += `${t("vmLxc.logs.node")}: ${data.node}\n` - logText += `${t("vmLxc.logs.type")}: ${data.type}\n` - logText += `${t("vmLxc.logs.totalLines")}: ${data.log_lines}\n` - logText += `${t("vmLxc.logs.generated")}: ${new Date().toISOString()}\n` - logText += `\n${"=".repeat(80)}\n\n` - - if (data.logs && Array.isArray(data.logs)) { - data.logs.forEach((log: any) => { - if (typeof log === "object" && log.t) { - logText += `${log.t}\n` - } else if (typeof log === "string") { - logText += `${log}\n` - } - }) - } - - const blob = new Blob([logText], { type: "text/plain" }) - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = `${vmName}-${vmid}-logs.txt` - a.click() - URL.revokeObjectURL(url) - } catch (error) { - console.error("Error downloading logs:", error) - } - } - const getStatusColor = (status: string) => { switch (status) { case "running": @@ -7852,25 +7815,6 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { - {/* Notification Row */} -
- - -
- {/* Protected Checkbox */}
= 90 and + if entry['mem_percent'] >= self.MEMORY_CRITICAL and current_time - entry['time'] <= self.MEMORY_DURATION ] @@ -1671,7 +1671,7 @@ class HealthMonitor: oldest = min(s['time'] for s in mem_critical_samples) actual_duration = int(current_time - oldest) status = 'CRITICAL' - reason = f'RAM >90% sustained for {actual_duration}s' + reason = f'RAM >{self.MEMORY_CRITICAL}% sustained for {actual_duration}s' elif swap_critical >= 2: status = 'CRITICAL' reason = (f'Memory pressure: swap {swap_percent:.0f}% used ' @@ -1691,7 +1691,7 @@ class HealthMonitor: swap_total_gb = round(swap.total / (1024**3), 2) # Determine per-sub-check status - ram_status = 'CRITICAL' if mem_percent >= 90 and mem_critical_count >= MEM_CRITICAL_MIN_SAMPLES else ('WARNING' if mem_percent >= self.MEMORY_WARNING and mem_warning_count >= MEM_WARNING_MIN_SAMPLES else 'OK') + ram_status = 'CRITICAL' if mem_percent >= self.MEMORY_CRITICAL and mem_critical_count >= MEM_CRITICAL_MIN_SAMPLES else ('WARNING' if mem_percent >= self.MEMORY_WARNING and mem_warning_count >= MEM_WARNING_MIN_SAMPLES else 'OK') swap_status = 'CRITICAL' if swap_critical >= 2 else 'OK' result = { diff --git a/AppImage/scripts/jwt_middleware.py b/AppImage/scripts/jwt_middleware.py index f78c063c..fb9b071f 100644 --- a/AppImage/scripts/jwt_middleware.py +++ b/AppImage/scripts/jwt_middleware.py @@ -8,6 +8,8 @@ from flask import request, jsonify, g from functools import wraps from auth_manager import load_auth_config, verify_token, verify_token_full +_READ_METHODS = ('GET', 'HEAD', 'OPTIONS') + def require_auth(f): """ @@ -64,12 +66,18 @@ def require_auth(f): token = parts[1] # Verify token - username = verify_token(token) + username, scope = verify_token_full(token) if not username: return jsonify({ "error": "Invalid or expired token", "message": "Please log in again" }), 401 + + # read_only tokens may only read; any method that can change + # state needs full_admin. + if request.method not in _READ_METHODS and scope != 'full_admin': + return jsonify({"error": "Insufficient scope", + "message": f"This action requires a full_admin token (your token: {scope})"}), 403 # Token is valid, allow access return f(*args, **kwargs) diff --git a/AppImage/scripts/notification_templates.py b/AppImage/scripts/notification_templates.py index b2751778..d0d87097 100644 --- a/AppImage/scripts/notification_templates.py +++ b/AppImage/scripts/notification_templates.py @@ -3335,6 +3335,7 @@ def _ai_cache_key(title, body, ai_config, detail_level, use_emojis): str(ai_config.get('ai_provider', '')), '\x1f', str(ai_config.get('ai_model', '')), '\x1f', str(ai_config.get('ai_language', '')), '\x1f', + str(ai_config.get('channel_type', '')).lower(), '\x1f', detail_level, '\x1f', '1' if use_emojis else '0', ] return _hash_ai_cache.sha256(''.join(parts).encode('utf-8', 'replace')).hexdigest() @@ -3428,7 +3429,8 @@ def format_with_ai_full(title: str, body: str, severity: str, channel_type = ai_config.get('channel_type', '').lower() is_email = channel_type == 'email' - if is_email and detail_level == 'detailed' and body and len(body) > 50: + if (is_email and detail_level == 'detailed' and body and len(body) > 50 + and result_body != body): # Only append if original has substantial content result_body += "\n\n" + "-" * 40 + "\n" result_body += "Original message:\n" diff --git a/web/messages/es/docs/hardware/nvidia-host.json b/web/messages/es/docs/hardware/nvidia-host.json index 51a259e5..b3fd9d63 100644 --- a/web/messages/es/docs/hardware/nvidia-host.json +++ b/web/messages/es/docs/hardware/nvidia-host.json @@ -109,7 +109,7 @@ "Regenera initramfs para todos los kernels, ejecuta proxmox-boot-tool refresh según la configuración de arranque y solicita reiniciar." ], "lxcWarnTitle": "LXC con acceso a NVIDIA", - "lxcWarnBody": "Retirar el controlador del host interrumpe las apps LXC que usan NVIDIA, aunque conserven sus bibliotecas. Programa mantenimiento para Frigate, Plex, Jellyfin, Ollama y otras apps que dependan de la GPU." + "lxcWarnBody": "Eliminar el controlador del host interrumpe las apps LXC que usan NVIDIA, aunque conserven sus bibliotecas. Programa mantenimiento para Frigate, Plex, Jellyfin, Ollama y otras apps que dependan de la GPU." }, "updates": { "heading": "Avisos de actualizaciones",