mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +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 { Progress } from "./ui/progress"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "./ui/dialog"
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
import { Checkbox } from "./ui/checkbox"
|
import { Checkbox } from "./ui/checkbox"
|
||||||
import { Switch } from "./ui/switch"
|
import { Switch } from "./ui/switch"
|
||||||
@@ -963,7 +963,6 @@ export function VirtualMachines() {
|
|||||||
const [showBackupModal, setShowBackupModal] = useState(false)
|
const [showBackupModal, setShowBackupModal] = useState(false)
|
||||||
const [backupMode, setBackupMode] = useState<string>("snapshot")
|
const [backupMode, setBackupMode] = useState<string>("snapshot")
|
||||||
const [backupProtected, setBackupProtected] = useState(false)
|
const [backupProtected, setBackupProtected] = useState(false)
|
||||||
const [backupNotification, setBackupNotification] = useState<string>("auto")
|
|
||||||
const [backupNotes, setBackupNotes] = useState<string>("{{guestname}}")
|
const [backupNotes, setBackupNotes] = useState<string>("{{guestname}}")
|
||||||
const [backupPbsChangeMode, setBackupPbsChangeMode] = useState<string>("default")
|
const [backupPbsChangeMode, setBackupPbsChangeMode] = useState<string>("default")
|
||||||
|
|
||||||
@@ -1698,7 +1697,6 @@ export function VirtualMachines() {
|
|||||||
// Reset modal to defaults
|
// Reset modal to defaults
|
||||||
setBackupMode("snapshot")
|
setBackupMode("snapshot")
|
||||||
setBackupProtected(false)
|
setBackupProtected(false)
|
||||||
setBackupNotification("auto")
|
|
||||||
setBackupNotes("{{guestname}}")
|
setBackupNotes("{{guestname}}")
|
||||||
setBackupPbsChangeMode("default")
|
setBackupPbsChangeMode("default")
|
||||||
// Auto-select first storage if none selected
|
// Auto-select first storage if none selected
|
||||||
@@ -1722,7 +1720,6 @@ export function VirtualMachines() {
|
|||||||
mode: backupMode,
|
mode: backupMode,
|
||||||
compress: "zstd",
|
compress: "zstd",
|
||||||
protected: backupProtected,
|
protected: backupProtected,
|
||||||
notification: backupNotification,
|
|
||||||
notes: backupNotes,
|
notes: backupNotes,
|
||||||
pbs_change_detection: backupPbsChangeMode
|
pbs_change_detection: backupPbsChangeMode
|
||||||
}),
|
}),
|
||||||
@@ -1879,40 +1876,6 @@ export function VirtualMachines() {
|
|||||||
setTerminalOpen(true)
|
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) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "running":
|
case "running":
|
||||||
@@ -7852,25 +7815,6 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Protected Checkbox */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -2105,7 +2105,7 @@
|
|||||||
"webhookUrl": "Webhook-URL",
|
"webhookUrl": "Webhook-URL",
|
||||||
"enableEmail": "E-Mail aktivieren",
|
"enableEmail": "E-Mail aktivieren",
|
||||||
"smtpHost": "SMTP-Host",
|
"smtpHost": "SMTP-Host",
|
||||||
"port": "Hafen",
|
"port": "Port",
|
||||||
"tlsMode": "TLS-Modus",
|
"tlsMode": "TLS-Modus",
|
||||||
"noTls": "Keine (Port 25)",
|
"noTls": "Keine (Port 25)",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
@@ -2578,9 +2578,9 @@
|
|||||||
"twoFactor": {
|
"twoFactor": {
|
||||||
"title": "Zwei-Faktor-Authentifizierung",
|
"title": "Zwei-Faktor-Authentifizierung",
|
||||||
"hint": "Fügen Sie nach dem Passwort einen Einmalcode aus einer Authentifizierungs-App hinzu.",
|
"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.",
|
"enabled": "2FA ist aktiviert.",
|
||||||
"disable": "Deaktivieren Sie 2FA",
|
"disable": "2FA deaktivieren",
|
||||||
"disableTitle": "Deaktivieren Sie die Zwei-Faktor-Authentifizierung",
|
"disableTitle": "Deaktivieren Sie die Zwei-Faktor-Authentifizierung",
|
||||||
"disableDescription": "Bestätigen Sie mit Ihrem Passwort und einem aktuellen 2FA-Code oder Backup-Code.",
|
"disableDescription": "Bestätigen Sie mit Ihrem Passwort und einem aktuellen 2FA-Code oder Backup-Code.",
|
||||||
"codeOrBackup": "2FA-Code oder Backup-Code",
|
"codeOrBackup": "2FA-Code oder Backup-Code",
|
||||||
@@ -2723,7 +2723,7 @@
|
|||||||
"interfaceOptional": "Schnittstelle (optional)",
|
"interfaceOptional": "Schnittstelle (optional)",
|
||||||
"applyTo": "Gelten",
|
"applyTo": "Gelten",
|
||||||
"commentOptional": "Kommentar (optional)",
|
"commentOptional": "Kommentar (optional)",
|
||||||
"port": "Hafen",
|
"port": "Port",
|
||||||
"source": "Quelle",
|
"source": "Quelle",
|
||||||
"level": "Ebene",
|
"level": "Ebene",
|
||||||
"interface": "Schnittstelle",
|
"interface": "Schnittstelle",
|
||||||
@@ -3015,7 +3015,7 @@
|
|||||||
"needQrOrKey": "Ein QR-Code oder manueller Schlüssel aus diesem Setup.",
|
"needQrOrKey": "Ein QR-Code oder manueller Schlüssel aus diesem Setup.",
|
||||||
"needBackupCodes": "Backup-Codes werden an einem sicheren Ort gespeichert.",
|
"needBackupCodes": "Backup-Codes werden an einem sicheren Ort gespeichert.",
|
||||||
"starting": "Beginnt...",
|
"starting": "Beginnt...",
|
||||||
"startSetup": "Starten Sie die Einrichtung",
|
"startSetup": "Einrichtung starten",
|
||||||
"scanTitle": "Scannen Sie den QR-Code",
|
"scanTitle": "Scannen Sie den QR-Code",
|
||||||
"scanDescription": "Öffnen Sie Ihre Authentifizierungs-App und scannen Sie diesen Code.",
|
"scanDescription": "Öffnen Sie Ihre Authentifizierungs-App und scannen Sie diesen Code.",
|
||||||
"qrCodeAlt": "2FA QR-Code",
|
"qrCodeAlt": "2FA QR-Code",
|
||||||
|
|||||||
@@ -3013,7 +3013,7 @@
|
|||||||
"youWillNeed": "Vous aurez besoin de :",
|
"youWillNeed": "Vous aurez besoin de :",
|
||||||
"needApp": "Une application d'authentification telle que Aegis, 1Password, Bitwarden ou Google Authenticator.",
|
"needApp": "Une application d'authentification telle que Aegis, 1Password, Bitwarden ou Google Authenticator.",
|
||||||
"needQrOrKey": "Un code QR ou une clé manuelle de cette configuration.",
|
"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...",
|
"starting": "Départ...",
|
||||||
"startSetup": "Démarrer la configuration",
|
"startSetup": "Démarrer la configuration",
|
||||||
"scanTitle": "Scannez le code QR",
|
"scanTitle": "Scannez le code QR",
|
||||||
@@ -3026,10 +3026,10 @@
|
|||||||
"verifying": "Vérification...",
|
"verifying": "Vérification...",
|
||||||
"verifyAndEnable": "Vérifier et activer",
|
"verifyAndEnable": "Vérifier et activer",
|
||||||
"enabledTitle": "2FA est activé.",
|
"enabledTitle": "2FA est activé.",
|
||||||
"enabledDescription": "Enregistrez les codes de sauvegarde avant de fermer cette fenêtre.",
|
"enabledDescription": "Enregistrez les codes de secours avant de fermer cette fenêtre.",
|
||||||
"saveCodesTitle": "Enregistrez vos codes de sauvegarde",
|
"saveCodesTitle": "Enregistrez vos codes de secours",
|
||||||
"saveCodesDescription": "Chaque code peut être utilisé une fois si vous perdez l'accès à votre application d'authentification.",
|
"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",
|
"copyAll": "Copier tout",
|
||||||
"finish": "Finition"
|
"finish": "Finition"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1086,7 +1086,7 @@
|
|||||||
"start": "Iniciar",
|
"start": "Iniciar",
|
||||||
"shutdown": "Desligar",
|
"shutdown": "Desligar",
|
||||||
"reboot": "Reiniciar",
|
"reboot": "Reiniciar",
|
||||||
"forceStop": "Forçar parada",
|
"forceStop": "Forçar paragem",
|
||||||
"working": "Processando...",
|
"working": "Processando...",
|
||||||
"startPending": "Iniciando...",
|
"startPending": "Iniciando...",
|
||||||
"shutdownPending": "Desligando...",
|
"shutdownPending": "Desligando...",
|
||||||
@@ -1094,7 +1094,7 @@
|
|||||||
"stopPending": "Parando..."
|
"stopPending": "Parando..."
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"forceStopTitle": "Forçar parada",
|
"forceStopTitle": "Forçar paragem",
|
||||||
"rebootTitle": "Reinício",
|
"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.",
|
"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.",
|
"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",
|
"port": "Porta",
|
||||||
"tlsMode": "Modo TLS",
|
"tlsMode": "Modo TLS",
|
||||||
"noTls": "Nenhum (porta 25)",
|
"noTls": "Nenhum (porta 25)",
|
||||||
"username": "Nome de usuário",
|
"username": "Nome de utilizador",
|
||||||
"password": "Senha",
|
"password": "Palavra-passe",
|
||||||
"appPassword": "Senha do aplicativo",
|
"appPassword": "Senha do aplicativo",
|
||||||
"fromAddress": "Do endereço",
|
"fromAddress": "Do endereço",
|
||||||
"toAddresses": "Para endereços (separados por vírgula)",
|
"toAddresses": "Para endereços (separados por vírgula)",
|
||||||
@@ -2369,8 +2369,8 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"subtitle": "Faça login para acessar seu painel",
|
"subtitle": "Faça login para acessar seu painel",
|
||||||
"username": "Nome de usuário",
|
"username": "Nome de utilizador",
|
||||||
"password": "Senha",
|
"password": "Palavra-passe",
|
||||||
"usernamePlaceholder": "Digite seu nome de usuário",
|
"usernamePlaceholder": "Digite seu nome de usuário",
|
||||||
"passwordPlaceholder": "Digite sua senha",
|
"passwordPlaceholder": "Digite sua senha",
|
||||||
"rememberMe": "Lembrar",
|
"rememberMe": "Lembrar",
|
||||||
@@ -2411,9 +2411,9 @@
|
|||||||
"skipFailed": "Falha ao ignorar a autenticação",
|
"skipFailed": "Falha ao ignorar a autenticação",
|
||||||
"savePreferenceFailed": "Falha ao salvar preferência",
|
"savePreferenceFailed": "Falha ao salvar preferência",
|
||||||
"setupFailed": "Falha ao configurar a autenticação",
|
"setupFailed": "Falha ao configurar a autenticação",
|
||||||
"username": "Nome de usuário",
|
"username": "Nome de utilizador",
|
||||||
"usernamePlaceholder": "Digite o nome de usuário",
|
"usernamePlaceholder": "Digite o nome de usuário",
|
||||||
"password": "Senha",
|
"password": "Palavra-passe",
|
||||||
"passwordPlaceholder": "Digite a senha",
|
"passwordPlaceholder": "Digite a senha",
|
||||||
"confirmPassword": "Confirme sua senha",
|
"confirmPassword": "Confirme sua senha",
|
||||||
"confirmPasswordPlaceholder": "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.",
|
"enableHint": "Adicione um nome de usuário e uma senha antes de expor este painel fora da sua rede privada.",
|
||||||
"enable": "Habilitar autenticação",
|
"enable": "Habilitar autenticação",
|
||||||
"setupTitle": "Criar login",
|
"setupTitle": "Criar login",
|
||||||
"username": "Nome de usuário",
|
"username": "Nome de utilizador",
|
||||||
"usernamePlaceholder": "administrador",
|
"usernamePlaceholder": "administrador",
|
||||||
"password": "Senha",
|
"password": "Palavra-passe",
|
||||||
"passwordPlaceholder": "Digite a senha",
|
"passwordPlaceholder": "Digite a senha",
|
||||||
"confirmPassword": "Confirme sua senha",
|
"confirmPassword": "Confirme sua senha",
|
||||||
"confirmPasswordPlaceholder": "Digite a senha novamente",
|
"confirmPasswordPlaceholder": "Digite a senha novamente",
|
||||||
@@ -2578,7 +2578,7 @@
|
|||||||
"twoFactor": {
|
"twoFactor": {
|
||||||
"title": "Autenticação de dois fatores",
|
"title": "Autenticação de dois fatores",
|
||||||
"hint": "Adicione um código único de um aplicativo autenticador após a senha.",
|
"hint": "Adicione um código único de um aplicativo autenticador após a senha.",
|
||||||
"enable": "Habilitar 2FA",
|
"enable": "Ativar 2FA",
|
||||||
"enabled": "2FA está habilitado.",
|
"enabled": "2FA está habilitado.",
|
||||||
"disable": "Desativar 2FA",
|
"disable": "Desativar 2FA",
|
||||||
"disableTitle": "Desative a autenticação de dois fatores",
|
"disableTitle": "Desative a autenticação de dois fatores",
|
||||||
@@ -4283,7 +4283,7 @@
|
|||||||
"onCalendarExpression": "Expressão OnCalendar",
|
"onCalendarExpression": "Expressão OnCalendar",
|
||||||
"ownerLabel": "Proprietário",
|
"ownerLabel": "Proprietário",
|
||||||
"passphrase": "Senha",
|
"passphrase": "Senha",
|
||||||
"password": "Senha",
|
"password": "Palavra-passe",
|
||||||
"pathLabel": "Caminho",
|
"pathLabel": "Caminho",
|
||||||
"pbsRepository": "Repositório PBS",
|
"pbsRepository": "Repositório PBS",
|
||||||
"profileLabel": "Perfil",
|
"profileLabel": "Perfil",
|
||||||
@@ -4316,7 +4316,7 @@
|
|||||||
"timeOfDay": "Hora do dia",
|
"timeOfDay": "Hora do dia",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"used": "Usado",
|
"used": "Usado",
|
||||||
"username": "Nome de usuário",
|
"username": "Nome de utilizador",
|
||||||
"vmsLxcs": "VMs/LXCs",
|
"vmsLxcs": "VMs/LXCs",
|
||||||
"when": "Quando",
|
"when": "Quando",
|
||||||
"whenLabel": "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."
|
"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": {
|
"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."
|
"help": "Este é o nome de login. Para alterá-lo, desative a autenticação e configure-a novamente em Segurança."
|
||||||
},
|
},
|
||||||
"displayName": {
|
"displayName": {
|
||||||
@@ -7234,7 +7234,7 @@
|
|||||||
"latency": "Latência",
|
"latency": "Latência",
|
||||||
"event": "Evento",
|
"event": "Evento",
|
||||||
"sourceIp": "IP de origem",
|
"sourceIp": "IP de origem",
|
||||||
"username": "Nome de usuário",
|
"username": "Nome de utilizador",
|
||||||
"service": "Serviço",
|
"service": "Serviço",
|
||||||
"jail": "Jail",
|
"jail": "Jail",
|
||||||
"failures": "Falhas",
|
"failures": "Falhas",
|
||||||
|
|||||||
@@ -2105,7 +2105,7 @@
|
|||||||
"webhookUrl": "Webhook URL",
|
"webhookUrl": "Webhook URL",
|
||||||
"enableEmail": "Aktivera e-post",
|
"enableEmail": "Aktivera e-post",
|
||||||
"smtpHost": "SMTP-värd",
|
"smtpHost": "SMTP-värd",
|
||||||
"port": "Hamn",
|
"port": "Port",
|
||||||
"tlsMode": "TLS-läge",
|
"tlsMode": "TLS-läge",
|
||||||
"noTls": "Ingen (port 25)",
|
"noTls": "Ingen (port 25)",
|
||||||
"username": "Användarnamn",
|
"username": "Användarnamn",
|
||||||
@@ -2723,7 +2723,7 @@
|
|||||||
"interfaceOptional": "Gränssnitt (valfritt)",
|
"interfaceOptional": "Gränssnitt (valfritt)",
|
||||||
"applyTo": "Ansök till",
|
"applyTo": "Ansök till",
|
||||||
"commentOptional": "Kommentar (valfritt)",
|
"commentOptional": "Kommentar (valfritt)",
|
||||||
"port": "Hamn",
|
"port": "Port",
|
||||||
"source": "Källa",
|
"source": "Källa",
|
||||||
"level": "Nivå",
|
"level": "Nivå",
|
||||||
"interface": "Gränssnitt",
|
"interface": "Gränssnitt",
|
||||||
|
|||||||
@@ -560,10 +560,12 @@ def totp_setup():
|
|||||||
"""Initialize TOTP setup for a user"""
|
"""Initialize TOTP setup for a user"""
|
||||||
try:
|
try:
|
||||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
username = auth_manager.verify_token(token)
|
username, scope = auth_manager.verify_token_full(token)
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
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)
|
success, secret, qr_code, backup_codes, message = auth_manager.setup_totp(username)
|
||||||
|
|
||||||
@@ -586,10 +588,12 @@ def totp_enable():
|
|||||||
"""Enable TOTP after verification"""
|
"""Enable TOTP after verification"""
|
||||||
try:
|
try:
|
||||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
username = auth_manager.verify_token(token)
|
username, scope = auth_manager.verify_token_full(token)
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
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
|
data = request.json
|
||||||
verification_token = data.get('token')
|
verification_token = data.get('token')
|
||||||
@@ -612,10 +616,12 @@ def totp_disable():
|
|||||||
"""Disable TOTP (requires password confirmation)"""
|
"""Disable TOTP (requires password confirmation)"""
|
||||||
try:
|
try:
|
||||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
username = auth_manager.verify_token(token)
|
username, scope = auth_manager.verify_token_full(token)
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
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 {}
|
data = request.json or {}
|
||||||
password = data.get('password')
|
password = data.get('password')
|
||||||
@@ -653,10 +659,12 @@ def generate_api_token():
|
|||||||
if not token:
|
if not token:
|
||||||
return jsonify({"success": False, "message": "Unauthorized. Please log in first."}), 401
|
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:
|
if not username:
|
||||||
return jsonify({"success": False, "message": "Invalid or expired session. Please log in again."}), 401
|
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
|
data = request.json
|
||||||
password = data.get('password')
|
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
|
return jsonify({"success": False, "message": "Authentication is not configured"}), 400
|
||||||
|
|
||||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
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
|
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)
|
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['used'] = round(partition_usage.used / (1024**3), 1)
|
||||||
disk_info['available'] = round(partition_usage.free / (1024**3), 1)
|
disk_info['available'] = round(partition_usage.free / (1024**3), 1)
|
||||||
disk_info['usage_percent'] = round(partition_usage.percent, 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:
|
except PermissionError:
|
||||||
continue
|
continue
|
||||||
@@ -13095,7 +13097,6 @@ def api_create_backup(vmid):
|
|||||||
mode = data.get('mode', 'snapshot') # snapshot, suspend, stop
|
mode = data.get('mode', 'snapshot') # snapshot, suspend, stop
|
||||||
compress = data.get('compress', 'zstd') # none, lzo, gzip, zstd
|
compress = data.get('compress', 'zstd') # none, lzo, gzip, zstd
|
||||||
protected = data.get('protected', False) # True/False
|
protected = data.get('protected', False) # True/False
|
||||||
notification = data.get('notification', 'auto') # always, failure, never, auto
|
|
||||||
notes = data.get('notes', '') # Backup notes/description
|
notes = data.get('notes', '') # Backup notes/description
|
||||||
pbs_change_detection = data.get('pbs_change_detection', None) # default, legacy, data (for PBS + LXC)
|
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()
|
traceback.print_exc()
|
||||||
return jsonify({'error': str(e)}), 500
|
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'])
|
@app.route('/api/health', methods=['GET'])
|
||||||
@require_auth
|
@require_auth
|
||||||
def api_health():
|
def api_health():
|
||||||
@@ -13621,7 +13635,7 @@ def api_health():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'healthy',
|
'status': 'healthy',
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
'version': '1.2.2'
|
'version': _installed_proxmenux_version()
|
||||||
})
|
})
|
||||||
|
|
||||||
# ─── User-configurable health thresholds ─────────────────────────────────────
|
# ─── User-configurable health thresholds ─────────────────────────────────────
|
||||||
@@ -15047,11 +15061,11 @@ def api_prometheus():
|
|||||||
disk_name = disk.get('name', 'unknown')
|
disk_name = disk.get('name', 'unknown')
|
||||||
metrics.append(f'# HELP proxmox_disk_total_bytes Total disk space in bytes')
|
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'# 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'# HELP proxmox_disk_used_bytes Used disk space in bytes')
|
||||||
metrics.append(f'# TYPE proxmox_disk_used_bytes gauge')
|
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'# HELP proxmox_disk_usage_percent Disk usage percentage')
|
||||||
metrics.append(f'# TYPE proxmox_disk_usage_percent gauge')
|
metrics.append(f'# TYPE proxmox_disk_usage_percent gauge')
|
||||||
|
|||||||
@@ -1637,7 +1637,7 @@ class HealthMonitor:
|
|||||||
|
|
||||||
mem_critical_samples = [
|
mem_critical_samples = [
|
||||||
entry for entry in self.state_history[state_key]
|
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
|
current_time - entry['time'] <= self.MEMORY_DURATION
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1671,7 +1671,7 @@ class HealthMonitor:
|
|||||||
oldest = min(s['time'] for s in mem_critical_samples)
|
oldest = min(s['time'] for s in mem_critical_samples)
|
||||||
actual_duration = int(current_time - oldest)
|
actual_duration = int(current_time - oldest)
|
||||||
status = 'CRITICAL'
|
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:
|
elif swap_critical >= 2:
|
||||||
status = 'CRITICAL'
|
status = 'CRITICAL'
|
||||||
reason = (f'Memory pressure: swap {swap_percent:.0f}% used '
|
reason = (f'Memory pressure: swap {swap_percent:.0f}% used '
|
||||||
@@ -1691,7 +1691,7 @@ class HealthMonitor:
|
|||||||
swap_total_gb = round(swap.total / (1024**3), 2)
|
swap_total_gb = round(swap.total / (1024**3), 2)
|
||||||
|
|
||||||
# Determine per-sub-check status
|
# 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'
|
swap_status = 'CRITICAL' if swap_critical >= 2 else 'OK'
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from flask import request, jsonify, g
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from auth_manager import load_auth_config, verify_token, verify_token_full
|
from auth_manager import load_auth_config, verify_token, verify_token_full
|
||||||
|
|
||||||
|
_READ_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
||||||
|
|
||||||
|
|
||||||
def require_auth(f):
|
def require_auth(f):
|
||||||
"""
|
"""
|
||||||
@@ -64,12 +66,18 @@ def require_auth(f):
|
|||||||
token = parts[1]
|
token = parts[1]
|
||||||
|
|
||||||
# Verify token
|
# Verify token
|
||||||
username = verify_token(token)
|
username, scope = verify_token_full(token)
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"error": "Invalid or expired token",
|
"error": "Invalid or expired token",
|
||||||
"message": "Please log in again"
|
"message": "Please log in again"
|
||||||
}), 401
|
}), 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
|
# Token is valid, allow access
|
||||||
return f(*args, **kwargs)
|
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_provider', '')), '\x1f',
|
||||||
str(ai_config.get('ai_model', '')), '\x1f',
|
str(ai_config.get('ai_model', '')), '\x1f',
|
||||||
str(ai_config.get('ai_language', '')), '\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',
|
detail_level, '\x1f', '1' if use_emojis else '0',
|
||||||
]
|
]
|
||||||
return _hash_ai_cache.sha256(''.join(parts).encode('utf-8', 'replace')).hexdigest()
|
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()
|
channel_type = ai_config.get('channel_type', '').lower()
|
||||||
is_email = channel_type == 'email'
|
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
|
# Only append if original has substantial content
|
||||||
result_body += "\n\n" + "-" * 40 + "\n"
|
result_body += "\n\n" + "-" * 40 + "\n"
|
||||||
result_body += "Original message:\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."
|
"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",
|
"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": {
|
"updates": {
|
||||||
"heading": "Avisos de actualizaciones",
|
"heading": "Avisos de actualizaciones",
|
||||||
|
|||||||
Reference in New Issue
Block a user