mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
Remove the notification menu from the VM and LXC backup dialog; backups follow the notification settings
This commit is contained in:
@@ -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<string>("snapshot")
|
||||
const [backupProtected, setBackupProtected] = useState(false)
|
||||
const [backupNotification, setBackupNotification] = useState<string>("auto")
|
||||
const [backupNotes, setBackupNotes] = useState<string>("{{guestname}}")
|
||||
const [backupPbsChangeMode, setBackupPbsChangeMode] = useState<string>("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) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notification Row */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm flex items-center gap-1.5">
|
||||
<Bell className="h-3.5 w-3.5" />
|
||||
{t("vmLxc.backupModal.notification")}
|
||||
</Label>
|
||||
<Select value={backupNotification} onValueChange={setBackupNotification}>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="auto">{t("vmLxc.backupModal.useGlobalSettings")}</SelectItem>
|
||||
<SelectItem value="always">{t("vmLxc.backupModal.alwaysNotify")}</SelectItem>
|
||||
<SelectItem value="failure">{t("vmLxc.backupModal.notifyOnFailure")}</SelectItem>
|
||||
<SelectItem value="never">{t("vmLxc.backupModal.neverNotify")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Protected Checkbox */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
|
||||
@@ -2105,7 +2105,7 @@
|
||||
"webhookUrl": "Webhook-URL",
|
||||
"enableEmail": "E-Mail aktivieren",
|
||||
"smtpHost": "SMTP-Host",
|
||||
"port": "Hafen",
|
||||
"port": "Port",
|
||||
"tlsMode": "TLS-Modus",
|
||||
"noTls": "Keine (Port 25)",
|
||||
"username": "Benutzername",
|
||||
@@ -2578,9 +2578,9 @@
|
||||
"twoFactor": {
|
||||
"title": "Zwei-Faktor-Authentifizierung",
|
||||
"hint": "Fügen Sie nach dem Passwort einen Einmalcode aus einer Authentifizierungs-App hinzu.",
|
||||
"enable": "Aktivieren Sie 2FA",
|
||||
"enable": "2FA aktivieren",
|
||||
"enabled": "2FA ist aktiviert.",
|
||||
"disable": "Deaktivieren Sie 2FA",
|
||||
"disable": "2FA deaktivieren",
|
||||
"disableTitle": "Deaktivieren Sie die Zwei-Faktor-Authentifizierung",
|
||||
"disableDescription": "Bestätigen Sie mit Ihrem Passwort und einem aktuellen 2FA-Code oder Backup-Code.",
|
||||
"codeOrBackup": "2FA-Code oder Backup-Code",
|
||||
@@ -2723,7 +2723,7 @@
|
||||
"interfaceOptional": "Schnittstelle (optional)",
|
||||
"applyTo": "Gelten",
|
||||
"commentOptional": "Kommentar (optional)",
|
||||
"port": "Hafen",
|
||||
"port": "Port",
|
||||
"source": "Quelle",
|
||||
"level": "Ebene",
|
||||
"interface": "Schnittstelle",
|
||||
@@ -3015,7 +3015,7 @@
|
||||
"needQrOrKey": "Ein QR-Code oder manueller Schlüssel aus diesem Setup.",
|
||||
"needBackupCodes": "Backup-Codes werden an einem sicheren Ort gespeichert.",
|
||||
"starting": "Beginnt...",
|
||||
"startSetup": "Starten Sie die Einrichtung",
|
||||
"startSetup": "Einrichtung starten",
|
||||
"scanTitle": "Scannen Sie den QR-Code",
|
||||
"scanDescription": "Öffnen Sie Ihre Authentifizierungs-App und scannen Sie diesen Code.",
|
||||
"qrCodeAlt": "2FA QR-Code",
|
||||
|
||||
@@ -3013,7 +3013,7 @@
|
||||
"youWillNeed": "Vous aurez besoin de :",
|
||||
"needApp": "Une application d'authentification telle que Aegis, 1Password, Bitwarden ou Google Authenticator.",
|
||||
"needQrOrKey": "Un code QR ou une clé manuelle de cette configuration.",
|
||||
"needBackupCodes": "Codes de sauvegarde enregistrés dans un endroit sûr.",
|
||||
"needBackupCodes": "Codes de secours enregistrés dans un endroit sûr.",
|
||||
"starting": "Départ...",
|
||||
"startSetup": "Démarrer la configuration",
|
||||
"scanTitle": "Scannez le code QR",
|
||||
@@ -3026,10 +3026,10 @@
|
||||
"verifying": "Vérification...",
|
||||
"verifyAndEnable": "Vérifier et activer",
|
||||
"enabledTitle": "2FA est activé.",
|
||||
"enabledDescription": "Enregistrez les codes de sauvegarde avant de fermer cette fenêtre.",
|
||||
"saveCodesTitle": "Enregistrez vos codes de sauvegarde",
|
||||
"enabledDescription": "Enregistrez les codes de secours avant de fermer cette fenêtre.",
|
||||
"saveCodesTitle": "Enregistrez vos codes de secours",
|
||||
"saveCodesDescription": "Chaque code peut être utilisé une fois si vous perdez l'accès à votre application d'authentification.",
|
||||
"backupCodes": "Codes de sauvegarde",
|
||||
"backupCodes": "Codes de secours",
|
||||
"copyAll": "Copier tout",
|
||||
"finish": "Finition"
|
||||
},
|
||||
|
||||
@@ -1086,7 +1086,7 @@
|
||||
"start": "Iniciar",
|
||||
"shutdown": "Desligar",
|
||||
"reboot": "Reiniciar",
|
||||
"forceStop": "Forçar parada",
|
||||
"forceStop": "Forçar paragem",
|
||||
"working": "Processando...",
|
||||
"startPending": "Iniciando...",
|
||||
"shutdownPending": "Desligando...",
|
||||
@@ -1094,7 +1094,7 @@
|
||||
"stopPending": "Parando..."
|
||||
},
|
||||
"confirm": {
|
||||
"forceStopTitle": "Forçar parada",
|
||||
"forceStopTitle": "Forçar paragem",
|
||||
"rebootTitle": "Reinício",
|
||||
"forceStopDescription": "Isso ignora a sequência de desligamento do sistema operacional convidado e pode corromper bancos de dados ou sistemas de arquivos em execução. O convidado é morto imediatamente.",
|
||||
"rebootDescription": "Isso força uma reinicialização sem esperar que o sistema operacional convidado libere as gravações pendentes. Use um desligamento elegante quando possível.",
|
||||
@@ -2108,8 +2108,8 @@
|
||||
"port": "Porta",
|
||||
"tlsMode": "Modo TLS",
|
||||
"noTls": "Nenhum (porta 25)",
|
||||
"username": "Nome de usuário",
|
||||
"password": "Senha",
|
||||
"username": "Nome de utilizador",
|
||||
"password": "Palavra-passe",
|
||||
"appPassword": "Senha do aplicativo",
|
||||
"fromAddress": "Do endereço",
|
||||
"toAddresses": "Para endereços (separados por vírgula)",
|
||||
@@ -2369,8 +2369,8 @@
|
||||
},
|
||||
"login": {
|
||||
"subtitle": "Faça login para acessar seu painel",
|
||||
"username": "Nome de usuário",
|
||||
"password": "Senha",
|
||||
"username": "Nome de utilizador",
|
||||
"password": "Palavra-passe",
|
||||
"usernamePlaceholder": "Digite seu nome de usuário",
|
||||
"passwordPlaceholder": "Digite sua senha",
|
||||
"rememberMe": "Lembrar",
|
||||
@@ -2411,9 +2411,9 @@
|
||||
"skipFailed": "Falha ao ignorar a autenticação",
|
||||
"savePreferenceFailed": "Falha ao salvar preferência",
|
||||
"setupFailed": "Falha ao configurar a autenticação",
|
||||
"username": "Nome de usuário",
|
||||
"username": "Nome de utilizador",
|
||||
"usernamePlaceholder": "Digite o nome de usuário",
|
||||
"password": "Senha",
|
||||
"password": "Palavra-passe",
|
||||
"passwordPlaceholder": "Digite a senha",
|
||||
"confirmPassword": "Confirme sua senha",
|
||||
"confirmPasswordPlaceholder": "Confirme sua senha",
|
||||
@@ -2556,9 +2556,9 @@
|
||||
"enableHint": "Adicione um nome de usuário e uma senha antes de expor este painel fora da sua rede privada.",
|
||||
"enable": "Habilitar autenticação",
|
||||
"setupTitle": "Criar login",
|
||||
"username": "Nome de usuário",
|
||||
"username": "Nome de utilizador",
|
||||
"usernamePlaceholder": "administrador",
|
||||
"password": "Senha",
|
||||
"password": "Palavra-passe",
|
||||
"passwordPlaceholder": "Digite a senha",
|
||||
"confirmPassword": "Confirme sua senha",
|
||||
"confirmPasswordPlaceholder": "Digite a senha novamente",
|
||||
@@ -2578,7 +2578,7 @@
|
||||
"twoFactor": {
|
||||
"title": "Autenticação de dois fatores",
|
||||
"hint": "Adicione um código único de um aplicativo autenticador após a senha.",
|
||||
"enable": "Habilitar 2FA",
|
||||
"enable": "Ativar 2FA",
|
||||
"enabled": "2FA está habilitado.",
|
||||
"disable": "Desativar 2FA",
|
||||
"disableTitle": "Desative a autenticação de dois fatores",
|
||||
@@ -4283,7 +4283,7 @@
|
||||
"onCalendarExpression": "Expressão OnCalendar",
|
||||
"ownerLabel": "Proprietário",
|
||||
"passphrase": "Senha",
|
||||
"password": "Senha",
|
||||
"password": "Palavra-passe",
|
||||
"pathLabel": "Caminho",
|
||||
"pbsRepository": "Repositório PBS",
|
||||
"profileLabel": "Perfil",
|
||||
@@ -4316,7 +4316,7 @@
|
||||
"timeOfDay": "Hora do dia",
|
||||
"type": "Tipo",
|
||||
"used": "Usado",
|
||||
"username": "Nome de usuário",
|
||||
"username": "Nome de utilizador",
|
||||
"vmsLxcs": "VMs/LXCs",
|
||||
"when": "Quando",
|
||||
"whenLabel": "Quando",
|
||||
@@ -5001,7 +5001,7 @@
|
||||
"hint": "PNG, JPEG, WebP ou GIF. Até 2 MB. Use uma imagem quadrada ou corte-a primeiro para obter o melhor resultado no cabeçalho."
|
||||
},
|
||||
"username": {
|
||||
"label": "Nome de usuário",
|
||||
"label": "Nome de utilizador",
|
||||
"help": "Este é o nome de login. Para alterá-lo, desative a autenticação e configure-a novamente em Segurança."
|
||||
},
|
||||
"displayName": {
|
||||
@@ -7234,7 +7234,7 @@
|
||||
"latency": "Latência",
|
||||
"event": "Evento",
|
||||
"sourceIp": "IP de origem",
|
||||
"username": "Nome de usuário",
|
||||
"username": "Nome de utilizador",
|
||||
"service": "Serviço",
|
||||
"jail": "Jail",
|
||||
"failures": "Falhas",
|
||||
|
||||
@@ -2105,7 +2105,7 @@
|
||||
"webhookUrl": "Webhook URL",
|
||||
"enableEmail": "Aktivera e-post",
|
||||
"smtpHost": "SMTP-värd",
|
||||
"port": "Hamn",
|
||||
"port": "Port",
|
||||
"tlsMode": "TLS-läge",
|
||||
"noTls": "Ingen (port 25)",
|
||||
"username": "Användarnamn",
|
||||
@@ -2723,7 +2723,7 @@
|
||||
"interfaceOptional": "Gränssnitt (valfritt)",
|
||||
"applyTo": "Ansök till",
|
||||
"commentOptional": "Kommentar (valfritt)",
|
||||
"port": "Hamn",
|
||||
"port": "Port",
|
||||
"source": "Källa",
|
||||
"level": "Nivå",
|
||||
"interface": "Gränssnitt",
|
||||
|
||||
@@ -560,10 +560,12 @@ def totp_setup():
|
||||
"""Initialize TOTP setup for a user"""
|
||||
try:
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
username = auth_manager.verify_token(token)
|
||||
username, scope = auth_manager.verify_token_full(token)
|
||||
|
||||
if not username:
|
||||
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||
if scope != 'full_admin':
|
||||
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
|
||||
|
||||
success, secret, qr_code, backup_codes, message = auth_manager.setup_totp(username)
|
||||
|
||||
@@ -586,10 +588,12 @@ def totp_enable():
|
||||
"""Enable TOTP after verification"""
|
||||
try:
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
username = auth_manager.verify_token(token)
|
||||
username, scope = auth_manager.verify_token_full(token)
|
||||
|
||||
if not username:
|
||||
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||
if scope != 'full_admin':
|
||||
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
|
||||
|
||||
data = request.json
|
||||
verification_token = data.get('token')
|
||||
@@ -612,10 +616,12 @@ def totp_disable():
|
||||
"""Disable TOTP (requires password confirmation)"""
|
||||
try:
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
username = auth_manager.verify_token(token)
|
||||
username, scope = auth_manager.verify_token_full(token)
|
||||
|
||||
if not username:
|
||||
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||
if scope != 'full_admin':
|
||||
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
|
||||
|
||||
data = request.json or {}
|
||||
password = data.get('password')
|
||||
@@ -653,10 +659,12 @@ def generate_api_token():
|
||||
if not token:
|
||||
return jsonify({"success": False, "message": "Unauthorized. Please log in first."}), 401
|
||||
|
||||
username = auth_manager.verify_token(token)
|
||||
username, scope = auth_manager.verify_token_full(token)
|
||||
|
||||
if not username:
|
||||
return jsonify({"success": False, "message": "Invalid or expired session. Please log in again."}), 401
|
||||
if scope != 'full_admin':
|
||||
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
|
||||
|
||||
data = request.json
|
||||
password = data.get('password')
|
||||
@@ -749,8 +757,11 @@ def revoke_api_token_route(token_id):
|
||||
return jsonify({"success": False, "message": "Authentication is not configured"}), 400
|
||||
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
if not token or not auth_manager.verify_token(token):
|
||||
username, scope = auth_manager.verify_token_full(token) if token else (None, None)
|
||||
if not username:
|
||||
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||
if scope != 'full_admin':
|
||||
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
|
||||
|
||||
success, message = auth_manager.revoke_api_token(token_id)
|
||||
|
||||
|
||||
@@ -4549,6 +4549,8 @@ def get_storage_info():
|
||||
disk_info['used'] = round(partition_usage.used / (1024**3), 1)
|
||||
disk_info['available'] = round(partition_usage.free / (1024**3), 1)
|
||||
disk_info['usage_percent'] = round(partition_usage.percent, 1)
|
||||
disk_info['total_bytes'] = partition_usage.total
|
||||
disk_info['used_bytes'] = partition_usage.used
|
||||
|
||||
except PermissionError:
|
||||
continue
|
||||
@@ -13095,7 +13097,6 @@ def api_create_backup(vmid):
|
||||
mode = data.get('mode', 'snapshot') # snapshot, suspend, stop
|
||||
compress = data.get('compress', 'zstd') # none, lzo, gzip, zstd
|
||||
protected = data.get('protected', False) # True/False
|
||||
notification = data.get('notification', 'auto') # always, failure, never, auto
|
||||
notes = data.get('notes', '') # Backup notes/description
|
||||
pbs_change_detection = data.get('pbs_change_detection', None) # default, legacy, data (for PBS + LXC)
|
||||
|
||||
@@ -13614,6 +13615,19 @@ def get_task_log(upid):
|
||||
traceback.print_exc()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
def _installed_proxmenux_version():
|
||||
"""Installed ProxMenux version; a beta install keeps beta_version.txt."""
|
||||
for name in ('beta_version.txt', 'version.txt'):
|
||||
try:
|
||||
with open(f'/usr/local/share/proxmenux/{name}') as fh:
|
||||
version = fh.read().strip()
|
||||
if version:
|
||||
return version
|
||||
except OSError:
|
||||
continue
|
||||
return ''
|
||||
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
@require_auth
|
||||
def api_health():
|
||||
@@ -13621,7 +13635,7 @@ def api_health():
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'version': '1.2.2'
|
||||
'version': _installed_proxmenux_version()
|
||||
})
|
||||
|
||||
# ─── User-configurable health thresholds ─────────────────────────────────────
|
||||
@@ -15047,11 +15061,11 @@ def api_prometheus():
|
||||
disk_name = disk.get('name', 'unknown')
|
||||
metrics.append(f'# HELP proxmox_disk_total_bytes Total disk space in bytes')
|
||||
metrics.append(f'# TYPE proxmox_disk_total_bytes gauge')
|
||||
metrics.append(f'proxmox_disk_total_bytes{{node="{node}",disk="{disk_name}"}} {disk.get("total", 0)} {timestamp}')
|
||||
metrics.append(f'proxmox_disk_total_bytes{{node="{node}",disk="{disk_name}"}} {disk.get("total_bytes", 0)} {timestamp}')
|
||||
|
||||
metrics.append(f'# HELP proxmox_disk_used_bytes Used disk space in bytes')
|
||||
metrics.append(f'# TYPE proxmox_disk_used_bytes gauge')
|
||||
metrics.append(f'proxmox_disk_used_bytes{{node="{node}",disk="{disk_name}"}} {disk.get("used", 0)} {timestamp}')
|
||||
metrics.append(f'proxmox_disk_used_bytes{{node="{node}",disk="{disk_name}"}} {disk.get("used_bytes", 0)} {timestamp}')
|
||||
|
||||
metrics.append(f'# HELP proxmox_disk_usage_percent Disk usage percentage')
|
||||
metrics.append(f'# TYPE proxmox_disk_usage_percent gauge')
|
||||
|
||||
@@ -1637,7 +1637,7 @@ class HealthMonitor:
|
||||
|
||||
mem_critical_samples = [
|
||||
entry for entry in self.state_history[state_key]
|
||||
if entry['mem_percent'] >= 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 = {
|
||||
|
||||
@@ -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,13 +66,19 @@ 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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"Regenera <code>initramfs</code> para todos los kernels, ejecuta <code>proxmox-boot-tool refresh</code> 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",
|
||||
|
||||
Reference in New Issue
Block a user