mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
replication failure notifications
This commit is contained in:
@@ -512,6 +512,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
|||||||
}
|
}
|
||||||
return t("vmLxc.appEditor.upstreamErrorNetwork", { detail })
|
return t("vmLxc.appEditor.upstreamErrorNetwork", { detail })
|
||||||
}
|
}
|
||||||
|
if (lower.includes("github rate limited")) {
|
||||||
|
return t("vmLxc.appEditor.upstreamErrorGithubRateLimit")
|
||||||
|
}
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
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, Database, CloudOff, Code, X, Copy, Sparkles, ArrowUpCircle, BellOff, Globe2 } from "lucide-react"
|
import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff, Code, X, Copy, Sparkles, ArrowUpCircle, BellOff, Globe2, Github } from "lucide-react"
|
||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
import { NotificationSettings } from "./notification-settings"
|
import { NotificationSettings } from "./notification-settings"
|
||||||
@@ -392,6 +392,17 @@ export function Settings() {
|
|||||||
const [loadingInterfaces, setLoadingInterfaces] = useState(true)
|
const [loadingInterfaces, setLoadingInterfaces] = useState(true)
|
||||||
const [savingInterface, setSavingInterface] = useState<string | null>(null)
|
const [savingInterface, setSavingInterface] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Optional GitHub API authentication for app release/tag checks. The
|
||||||
|
// backend only returns whether a token exists; the secret itself never
|
||||||
|
// leaves the host after it has been saved.
|
||||||
|
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false)
|
||||||
|
const [githubTokenLoading, setGithubTokenLoading] = useState(true)
|
||||||
|
const [githubTokenEditMode, setGithubTokenEditMode] = useState(false)
|
||||||
|
const [githubTokenDraft, setGithubTokenDraft] = useState("")
|
||||||
|
const [githubTokenSaving, setGithubTokenSaving] = useState(false)
|
||||||
|
const [githubTokenSaved, setGithubTokenSaved] = useState(false)
|
||||||
|
const [githubTokenError, setGithubTokenError] = useState("")
|
||||||
|
|
||||||
// Active Suppressions panel — lists every error currently dismissed
|
// Active Suppressions panel — lists every error currently dismissed
|
||||||
// (time-limited or permanent) so the user can re-enable individual
|
// (time-limited or permanent) so the user can re-enable individual
|
||||||
// alerts. Mirrors what /api/health/full returns under `dismissed`.
|
// alerts. Mirrors what /api/health/full returns under `dismissed`.
|
||||||
@@ -451,6 +462,63 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadGithubTokenStatus = async () => {
|
||||||
|
setGithubTokenLoading(true)
|
||||||
|
try {
|
||||||
|
const data = await fetchApi<{ configured: boolean }>("/api/apps/github-token")
|
||||||
|
setGithubTokenConfigured(!!data.configured)
|
||||||
|
setGithubTokenError("")
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load GitHub API token status:", err)
|
||||||
|
setGithubTokenError(t("settings.githubApi.loadFailed"))
|
||||||
|
} finally {
|
||||||
|
setGithubTokenLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveGithubToken = async () => {
|
||||||
|
const token = githubTokenDraft.trim()
|
||||||
|
if (!token) return
|
||||||
|
setGithubTokenSaving(true)
|
||||||
|
setGithubTokenError("")
|
||||||
|
try {
|
||||||
|
await fetchApi<{ success: boolean; configured: boolean }>("/api/apps/github-token", {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
})
|
||||||
|
setGithubTokenConfigured(true)
|
||||||
|
setGithubTokenDraft("")
|
||||||
|
setGithubTokenEditMode(false)
|
||||||
|
setGithubTokenSaved(true)
|
||||||
|
window.setTimeout(() => setGithubTokenSaved(false), 2500)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save GitHub API token:", err)
|
||||||
|
setGithubTokenError(t("settings.githubApi.saveFailed"))
|
||||||
|
} finally {
|
||||||
|
setGithubTokenSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeGithubToken = async () => {
|
||||||
|
setGithubTokenSaving(true)
|
||||||
|
setGithubTokenError("")
|
||||||
|
try {
|
||||||
|
await fetchApi<{ success: boolean; configured: boolean }>("/api/apps/github-token", {
|
||||||
|
method: "DELETE",
|
||||||
|
})
|
||||||
|
setGithubTokenConfigured(false)
|
||||||
|
setGithubTokenDraft("")
|
||||||
|
setGithubTokenEditMode(false)
|
||||||
|
setGithubTokenSaved(true)
|
||||||
|
window.setTimeout(() => setGithubTokenSaved(false), 2500)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to remove GitHub API token:", err)
|
||||||
|
setGithubTokenError(t("settings.githubApi.removeFailed"))
|
||||||
|
} finally {
|
||||||
|
setGithubTokenSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProxmenuxTools()
|
loadProxmenuxTools()
|
||||||
getUnitsSettings()
|
getUnitsSettings()
|
||||||
@@ -459,6 +527,7 @@ export function Settings() {
|
|||||||
loadActiveSuppressions()
|
loadActiveSuppressions()
|
||||||
loadNetworkInterfaces()
|
loadNetworkInterfaces()
|
||||||
loadSnippetsStorage()
|
loadSnippetsStorage()
|
||||||
|
loadGithubTokenStatus()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Refresh the Active Suppressions list whenever:
|
// Refresh the Active Suppressions list whenever:
|
||||||
@@ -1803,6 +1872,113 @@ export function Settings() {
|
|||||||
is re-enabled). */}
|
is re-enabled). */}
|
||||||
<LxcUpdateDetection />
|
<LxcUpdateDetection />
|
||||||
|
|
||||||
|
{/* GitHub API — optional authentication for app upstream checks. */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Github className="h-5 w-5 text-foreground" />
|
||||||
|
<CardTitle>{t("settings.githubApi.title")}</CardTitle>
|
||||||
|
</div>
|
||||||
|
{!githubTokenLoading && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{githubTokenSaved && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-green-500">
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
{t("status.saved")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{githubTokenEditMode ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
setGithubTokenDraft("")
|
||||||
|
setGithubTokenError("")
|
||||||
|
setGithubTokenEditMode(false)
|
||||||
|
}}
|
||||||
|
disabled={githubTokenSaving}
|
||||||
|
>
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="h-7 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center gap-1.5"
|
||||||
|
onClick={saveGithubToken}
|
||||||
|
disabled={githubTokenSaving || !githubTokenDraft.trim()}
|
||||||
|
>
|
||||||
|
{githubTokenSaving ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
|
||||||
|
{t("actions.save")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1.5"
|
||||||
|
onClick={() => {
|
||||||
|
setGithubTokenError("")
|
||||||
|
setGithubTokenEditMode(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings2 className="h-3 w-3" />
|
||||||
|
{githubTokenConfigured ? t("actions.edit") : t("settings.githubApi.configure")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CardDescription>{t("settings.githubApi.description")}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className={githubTokenEditMode ? "bg-accent [&_input]:bg-background" : undefined}>
|
||||||
|
{githubTokenLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-6">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : githubTokenEditMode ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label htmlFor="github-api-token" className="text-sm font-medium text-foreground">
|
||||||
|
{t("settings.githubApi.tokenLabel")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="github-api-token"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={githubTokenDraft}
|
||||||
|
onChange={(event) => setGithubTokenDraft(event.target.value)}
|
||||||
|
placeholder={githubTokenConfigured ? "••••••••••••" : t("settings.githubApi.tokenPlaceholder")}
|
||||||
|
disabled={githubTokenSaving}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("settings.githubApi.tokenHelp")}</p>
|
||||||
|
</div>
|
||||||
|
{githubTokenConfigured && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-red-500/40 text-red-400 hover:bg-red-500/10 hover:text-red-300"
|
||||||
|
onClick={removeGithubToken}
|
||||||
|
disabled={githubTokenSaving}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 mr-2" />
|
||||||
|
{t("settings.githubApi.removeToken")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<span className={`h-2 w-2 rounded-full ${githubTokenConfigured ? "bg-green-500" : "bg-muted-foreground/60"}`} />
|
||||||
|
<span className={githubTokenConfigured ? "text-green-500" : "text-muted-foreground"}>
|
||||||
|
{githubTokenConfigured ? t("settings.githubApi.configured") : t("settings.githubApi.notConfigured")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{githubTokenError && (
|
||||||
|
<div className="mt-3 flex items-start gap-2 text-sm text-red-400">
|
||||||
|
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
|
||||||
|
<span>{githubTokenError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Notification Settings */}
|
{/* Notification Settings */}
|
||||||
<NotificationSettings />
|
<NotificationSettings />
|
||||||
|
|
||||||
|
|||||||
@@ -1527,6 +1527,7 @@
|
|||||||
"editButton": "Bearbeiten",
|
"editButton": "Bearbeiten",
|
||||||
"upstreamErrorTimeout": "Netzwerk-Timeout beim Kontaktieren des Upstreams",
|
"upstreamErrorTimeout": "Netzwerk-Timeout beim Kontaktieren des Upstreams",
|
||||||
"upstreamErrorNetwork": "Netzwerkfehler: {detail}",
|
"upstreamErrorNetwork": "Netzwerkfehler: {detail}",
|
||||||
|
"upstreamErrorGithubRateLimit": "Das GitHub-Anfragelimit wurde erreicht. Konfigurieren Sie unter Einstellungen → GitHub API ein optionales Token oder versuchen Sie es später erneut.",
|
||||||
"upstreamErrorGeneric": "Upstream-Prüfung fehlgeschlagen: {detail}",
|
"upstreamErrorGeneric": "Upstream-Prüfung fehlgeschlagen: {detail}",
|
||||||
"notificationsEnabled": "Upstream-Update-Benachrichtigungen EIN – zum Stummschalten klicken",
|
"notificationsEnabled": "Upstream-Update-Benachrichtigungen EIN – zum Stummschalten klicken",
|
||||||
"notificationsMuted": "Upstream-Update-Benachrichtigungen stummgeschaltet – zum Aktivieren klicken",
|
"notificationsMuted": "Upstream-Update-Benachrichtigungen stummgeschaltet – zum Aktivieren klicken",
|
||||||
@@ -1648,6 +1649,20 @@
|
|||||||
"saveFailed": "Die Einstellung für die LXC-Update-Erkennung konnte nicht gespeichert werden.",
|
"saveFailed": "Die Einstellung für die LXC-Update-Erkennung konnte nicht gespeichert werden.",
|
||||||
"purgedMessage": "{count} LXC-Einträge aus der Registrierung entfernt. Durch erneutes Aktivieren der Erkennung werden sie beim nächsten Scan-Zyklus neu aufgefüllt."
|
"purgedMessage": "{count} LXC-Einträge aus der Registrierung entfernt. Durch erneutes Aktivieren der Erkennung werden sie beim nächsten Scan-Zyklus neu aufgefüllt."
|
||||||
},
|
},
|
||||||
|
"githubApi": {
|
||||||
|
"title": "GitHub API",
|
||||||
|
"description": "Optionale Authentifizierung für die Release- und Tag-Prüfungen registrierter Anwendungen.",
|
||||||
|
"configure": "Konfigurieren",
|
||||||
|
"tokenLabel": "Persönliches Zugriffstoken",
|
||||||
|
"tokenPlaceholder": "github_pat_...",
|
||||||
|
"tokenHelp": "Das Token wird verschlüsselt gespeichert. ProxMenux verwendet es ausschließlich für schreibgeschützte GitHub-API-Anfragen und zeigt es danach nicht erneut an.",
|
||||||
|
"configured": "Token konfiguriert",
|
||||||
|
"notConfigured": "Anonymes GitHub-Anfragekontingent wird verwendet",
|
||||||
|
"removeToken": "Token entfernen",
|
||||||
|
"loadFailed": "Die GitHub-API-Einstellung konnte nicht geladen werden.",
|
||||||
|
"saveFailed": "Das GitHub-API-Token konnte nicht gespeichert werden.",
|
||||||
|
"removeFailed": "Das GitHub-API-Token konnte nicht entfernt werden."
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Benachrichtigungen",
|
"title": "Benachrichtigungen",
|
||||||
"description": "Konfigurieren Sie Benachrichtigungskanäle und Ereignisfilter. Erhalten Sie Benachrichtigungen per Telegram, Gotify, Discord oder E-Mail.",
|
"description": "Konfigurieren Sie Benachrichtigungskanäle und Ereignisfilter. Erhalten Sie Benachrichtigungen per Telegram, Gotify, Discord oder E-Mail.",
|
||||||
|
|||||||
@@ -1432,6 +1432,7 @@
|
|||||||
"updateAvailableBadge": "Update available",
|
"updateAvailableBadge": "Update available",
|
||||||
"upstreamErrorTimeout": "Network timeout while contacting upstream",
|
"upstreamErrorTimeout": "Network timeout while contacting upstream",
|
||||||
"upstreamErrorNetwork": "Network error: {detail}",
|
"upstreamErrorNetwork": "Network error: {detail}",
|
||||||
|
"upstreamErrorGithubRateLimit": "GitHub's request limit has been reached. Configure an optional token in Settings → GitHub API, or try again later.",
|
||||||
"upstreamErrorGeneric": "Upstream check failed: {detail}",
|
"upstreamErrorGeneric": "Upstream check failed: {detail}",
|
||||||
"portDescriptionPlaceholder": "Description (e.g. Web UI, go2rtc, admin)",
|
"portDescriptionPlaceholder": "Description (e.g. Web UI, go2rtc, admin)",
|
||||||
"portPortPlaceholder": "port",
|
"portPortPlaceholder": "port",
|
||||||
@@ -1647,6 +1648,20 @@
|
|||||||
"saveFailed": "Could not save the LXC update detection setting.",
|
"saveFailed": "Could not save the LXC update detection setting.",
|
||||||
"purgedMessage": "{count} LXC entries removed from the registry. Re-enabling detection will repopulate them on the next scan cycle."
|
"purgedMessage": "{count} LXC entries removed from the registry. Re-enabling detection will repopulate them on the next scan cycle."
|
||||||
},
|
},
|
||||||
|
"githubApi": {
|
||||||
|
"title": "GitHub API",
|
||||||
|
"description": "Optional authentication for the release and tag checks used by registered applications.",
|
||||||
|
"configure": "Configure",
|
||||||
|
"tokenLabel": "Personal access token",
|
||||||
|
"tokenPlaceholder": "github_pat_...",
|
||||||
|
"tokenHelp": "The token is stored encrypted. ProxMenux uses it only for read-only GitHub API requests and never displays it again.",
|
||||||
|
"configured": "Token configured",
|
||||||
|
"notConfigured": "Using GitHub's anonymous request quota",
|
||||||
|
"removeToken": "Remove token",
|
||||||
|
"loadFailed": "Could not load the GitHub API setting.",
|
||||||
|
"saveFailed": "Could not save the GitHub API token.",
|
||||||
|
"removeFailed": "Could not remove the GitHub API token."
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"description": "Configure notification channels and event filters. Receive alerts via Telegram, Gotify, Discord, or Email.",
|
"description": "Configure notification channels and event filters. Receive alerts via Telegram, Gotify, Discord, or Email.",
|
||||||
|
|||||||
@@ -1432,6 +1432,7 @@
|
|||||||
"upToDateBadge": "Actualizado",
|
"upToDateBadge": "Actualizado",
|
||||||
"upstreamErrorTimeout": "Tiempo de espera agotado al contactar con el origen",
|
"upstreamErrorTimeout": "Tiempo de espera agotado al contactar con el origen",
|
||||||
"upstreamErrorNetwork": "Error de red: {detail}",
|
"upstreamErrorNetwork": "Error de red: {detail}",
|
||||||
|
"upstreamErrorGithubRateLimit": "Se ha alcanzado el límite de solicitudes de GitHub. Configure un token opcional en Ajustes → API de GitHub o vuelva a intentarlo más tarde.",
|
||||||
"upstreamErrorGeneric": "Fallo al comprobar el origen: {detail}",
|
"upstreamErrorGeneric": "Fallo al comprobar el origen: {detail}",
|
||||||
"updateAvailableBadge": "Actualización disponible",
|
"updateAvailableBadge": "Actualización disponible",
|
||||||
"portDescriptionPlaceholder": "Descripción (por ejemplo, interfaz de usuario web, go2rtc, administrador)",
|
"portDescriptionPlaceholder": "Descripción (por ejemplo, interfaz de usuario web, go2rtc, administrador)",
|
||||||
@@ -1648,6 +1649,20 @@
|
|||||||
"saveFailed": "No se pudo guardar la configuración de detección de actualizaciones de LXC.",
|
"saveFailed": "No se pudo guardar la configuración de detección de actualizaciones de LXC.",
|
||||||
"purgedMessage": "{count} Entradas LXC eliminadas del registro. Al volver a habilitar la detección, se volverán a llenar en el siguiente ciclo de escaneo."
|
"purgedMessage": "{count} Entradas LXC eliminadas del registro. Al volver a habilitar la detección, se volverán a llenar en el siguiente ciclo de escaneo."
|
||||||
},
|
},
|
||||||
|
"githubApi": {
|
||||||
|
"title": "API de GitHub",
|
||||||
|
"description": "Autenticación opcional para comprobar los lanzamientos y las etiquetas de las aplicaciones registradas.",
|
||||||
|
"configure": "Configurar",
|
||||||
|
"tokenLabel": "Token de acceso personal",
|
||||||
|
"tokenPlaceholder": "github_pat_...",
|
||||||
|
"tokenHelp": "El token se guarda cifrado. ProxMenux solo lo utiliza para consultas de solo lectura a la API de GitHub y no vuelve a mostrarlo.",
|
||||||
|
"configured": "Token configurado",
|
||||||
|
"notConfigured": "Usando la cuota de solicitudes anónimas de GitHub",
|
||||||
|
"removeToken": "Eliminar token",
|
||||||
|
"loadFailed": "No se pudo cargar el ajuste de la API de GitHub.",
|
||||||
|
"saveFailed": "No se pudo guardar el token de la API de GitHub.",
|
||||||
|
"removeFailed": "No se pudo eliminar el token de la API de GitHub."
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notificaciones",
|
"title": "Notificaciones",
|
||||||
"description": "Configure canales de notificación y filtros de eventos. Recibe alertas vía Telegram, Gotify, Discord o Email.",
|
"description": "Configure canales de notificación y filtros de eventos. Recibe alertas vía Telegram, Gotify, Discord o Email.",
|
||||||
|
|||||||
@@ -1527,6 +1527,7 @@
|
|||||||
"editButton": "Modifier",
|
"editButton": "Modifier",
|
||||||
"upstreamErrorTimeout": "expiration du délai d'attente du réseau lors du contact en amont",
|
"upstreamErrorTimeout": "expiration du délai d'attente du réseau lors du contact en amont",
|
||||||
"upstreamErrorNetwork": "Erreur réseau : {detail}",
|
"upstreamErrorNetwork": "Erreur réseau : {detail}",
|
||||||
|
"upstreamErrorGithubRateLimit": "La limite de requêtes GitHub a été atteinte. Configurez un jeton facultatif dans Paramètres → API GitHub ou réessayez plus tard.",
|
||||||
"upstreamErrorGeneric": "Échec de la vérification en amont : {detail}",
|
"upstreamErrorGeneric": "Échec de la vérification en amont : {detail}",
|
||||||
"notificationsEnabled": "Notifications de mise à jour en amont activées – cliquez pour désactiver le son",
|
"notificationsEnabled": "Notifications de mise à jour en amont activées – cliquez pour désactiver le son",
|
||||||
"notificationsMuted": "Notifications de mise à jour en amont MUTED – cliquez pour activer",
|
"notificationsMuted": "Notifications de mise à jour en amont MUTED – cliquez pour activer",
|
||||||
@@ -1648,6 +1649,20 @@
|
|||||||
"saveFailed": "Impossible d'enregistrer le paramètre de détection de mise à jour LXC.",
|
"saveFailed": "Impossible d'enregistrer le paramètre de détection de mise à jour LXC.",
|
||||||
"purgedMessage": "{count} Entrées LXC supprimées du registre. La réactivation de la détection les repeuplera lors du prochain cycle d'analyse."
|
"purgedMessage": "{count} Entrées LXC supprimées du registre. La réactivation de la détection les repeuplera lors du prochain cycle d'analyse."
|
||||||
},
|
},
|
||||||
|
"githubApi": {
|
||||||
|
"title": "API GitHub",
|
||||||
|
"description": "Authentification facultative pour vérifier les versions et les étiquettes des applications enregistrées.",
|
||||||
|
"configure": "Configurer",
|
||||||
|
"tokenLabel": "Jeton d'accès personnel",
|
||||||
|
"tokenPlaceholder": "github_pat_...",
|
||||||
|
"tokenHelp": "Le jeton est stocké sous forme chiffrée. ProxMenux l'utilise uniquement pour des requêtes en lecture seule vers l'API GitHub et ne l'affiche plus ensuite.",
|
||||||
|
"configured": "Jeton configuré",
|
||||||
|
"notConfigured": "Utilisation du quota de requêtes anonymes de GitHub",
|
||||||
|
"removeToken": "Supprimer le jeton",
|
||||||
|
"loadFailed": "Impossible de charger le paramètre de l'API GitHub.",
|
||||||
|
"saveFailed": "Impossible d'enregistrer le jeton de l'API GitHub.",
|
||||||
|
"removeFailed": "Impossible de supprimer le jeton de l'API GitHub."
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"description": "Configurez les canaux de notification et les filtres d'événements. Recevez des alertes via Telegram, Gotify, Discord ou Email.",
|
"description": "Configurez les canaux de notification et les filtres d'événements. Recevez des alertes via Telegram, Gotify, Discord ou Email.",
|
||||||
|
|||||||
@@ -1527,6 +1527,7 @@
|
|||||||
"editButton": "Modificare",
|
"editButton": "Modificare",
|
||||||
"upstreamErrorTimeout": "timeout della rete durante il contatto a monte",
|
"upstreamErrorTimeout": "timeout della rete durante il contatto a monte",
|
||||||
"upstreamErrorNetwork": "errore di rete: {detail}",
|
"upstreamErrorNetwork": "errore di rete: {detail}",
|
||||||
|
"upstreamErrorGithubRateLimit": "È stato raggiunto il limite di richieste GitHub. Configura un token facoltativo in Impostazioni → API GitHub oppure riprova più tardi.",
|
||||||
"upstreamErrorGeneric": "controllo upstream non riuscito: {detail}",
|
"upstreamErrorGeneric": "controllo upstream non riuscito: {detail}",
|
||||||
"notificationsEnabled": "notifiche di aggiornamento upstream attivate: fai clic per disattivare l'audio",
|
"notificationsEnabled": "notifiche di aggiornamento upstream attivate: fai clic per disattivare l'audio",
|
||||||
"notificationsMuted": "notifiche di aggiornamento upstream MUTED: fare clic per abilitare",
|
"notificationsMuted": "notifiche di aggiornamento upstream MUTED: fare clic per abilitare",
|
||||||
@@ -1648,6 +1649,20 @@
|
|||||||
"saveFailed": "Impossibile salvare l'impostazione di rilevamento degli aggiornamenti LXC.",
|
"saveFailed": "Impossibile salvare l'impostazione di rilevamento degli aggiornamenti LXC.",
|
||||||
"purgedMessage": "{count} Voci LXC rimosse dal registro. La riattivazione del rilevamento li ripopolarà al ciclo di scansione successivo."
|
"purgedMessage": "{count} Voci LXC rimosse dal registro. La riattivazione del rilevamento li ripopolarà al ciclo di scansione successivo."
|
||||||
},
|
},
|
||||||
|
"githubApi": {
|
||||||
|
"title": "API GitHub",
|
||||||
|
"description": "Autenticazione facoltativa per controllare release e tag delle applicazioni registrate.",
|
||||||
|
"configure": "Configura",
|
||||||
|
"tokenLabel": "Token di accesso personale",
|
||||||
|
"tokenPlaceholder": "github_pat_...",
|
||||||
|
"tokenHelp": "Il token viene archiviato in forma cifrata. ProxMenux lo utilizza solo per richieste di sola lettura all'API GitHub e non lo mostra più dopo il salvataggio.",
|
||||||
|
"configured": "Token configurato",
|
||||||
|
"notConfigured": "Utilizzo della quota di richieste anonime di GitHub",
|
||||||
|
"removeToken": "Rimuovi token",
|
||||||
|
"loadFailed": "Impossibile caricare l'impostazione dell'API GitHub.",
|
||||||
|
"saveFailed": "Impossibile salvare il token dell'API GitHub.",
|
||||||
|
"removeFailed": "Impossibile rimuovere il token dell'API GitHub."
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notifiche",
|
"title": "Notifiche",
|
||||||
"description": "Configura canali di notifica e filtri eventi. Ricevi avvisi tramite Telegram, Gotify, Discord o e-mail.",
|
"description": "Configura canali di notifica e filtri eventi. Ricevi avvisi tramite Telegram, Gotify, Discord o e-mail.",
|
||||||
|
|||||||
@@ -1527,6 +1527,7 @@
|
|||||||
"editButton": "Editar",
|
"editButton": "Editar",
|
||||||
"upstreamErrorTimeout": "Tempo limite da rede ao entrar em contato com o upstream",
|
"upstreamErrorTimeout": "Tempo limite da rede ao entrar em contato com o upstream",
|
||||||
"upstreamErrorNetwork": "Erro de rede: {detail}",
|
"upstreamErrorNetwork": "Erro de rede: {detail}",
|
||||||
|
"upstreamErrorGithubRateLimit": "O limite de pedidos do GitHub foi atingido. Configure um token opcional em Definições → API do GitHub ou tente novamente mais tarde.",
|
||||||
"upstreamErrorGeneric": "falha na verificação upstream: {detail}",
|
"upstreamErrorGeneric": "falha na verificação upstream: {detail}",
|
||||||
"notificationsEnabled": "Notificações de atualização upstream ATIVADAS – clique para silenciar",
|
"notificationsEnabled": "Notificações de atualização upstream ATIVADAS – clique para silenciar",
|
||||||
"notificationsMuted": "notificações de atualização upstream silenciadas – clique para ativar",
|
"notificationsMuted": "notificações de atualização upstream silenciadas – clique para ativar",
|
||||||
@@ -1648,6 +1649,20 @@
|
|||||||
"saveFailed": "Não foi possível salvar a configuração de detecção de atualização LXC.",
|
"saveFailed": "Não foi possível salvar a configuração de detecção de atualização LXC.",
|
||||||
"purgedMessage": "{count} Entradas LXC removidas do registro. Reativar a detecção irá preenchê-los novamente no próximo ciclo de verificação."
|
"purgedMessage": "{count} Entradas LXC removidas do registro. Reativar a detecção irá preenchê-los novamente no próximo ciclo de verificação."
|
||||||
},
|
},
|
||||||
|
"githubApi": {
|
||||||
|
"title": "API do GitHub",
|
||||||
|
"description": "Autenticação opcional para verificar lançamentos e etiquetas das aplicações registadas.",
|
||||||
|
"configure": "Configurar",
|
||||||
|
"tokenLabel": "Token de acesso pessoal",
|
||||||
|
"tokenPlaceholder": "github_pat_...",
|
||||||
|
"tokenHelp": "O token é armazenado de forma cifrada. O ProxMenux utiliza-o apenas para pedidos de leitura à API do GitHub e não volta a apresentá-lo.",
|
||||||
|
"configured": "Token configurado",
|
||||||
|
"notConfigured": "A utilizar a quota de pedidos anónimos do GitHub",
|
||||||
|
"removeToken": "Remover token",
|
||||||
|
"loadFailed": "Não foi possível carregar a definição da API do GitHub.",
|
||||||
|
"saveFailed": "Não foi possível guardar o token da API do GitHub.",
|
||||||
|
"removeFailed": "Não foi possível remover o token da API do GitHub."
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notificações",
|
"title": "Notificações",
|
||||||
"description": "Configure canais de notificação e filtros de eventos. Receba alertas via Telegram, Gotify, Discord ou Email.",
|
"description": "Configure canais de notificação e filtros de eventos. Receba alertas via Telegram, Gotify, Discord ou Email.",
|
||||||
|
|||||||
@@ -1526,6 +1526,7 @@
|
|||||||
"editButton": "Upraviť",
|
"editButton": "Upraviť",
|
||||||
"upstreamErrorTimeout": "Časový limit siete pri kontaktovaní upstream",
|
"upstreamErrorTimeout": "Časový limit siete pri kontaktovaní upstream",
|
||||||
"upstreamErrorNetwork": "Chyba siete: {detail}",
|
"upstreamErrorNetwork": "Chyba siete: {detail}",
|
||||||
|
"upstreamErrorGithubRateLimit": "Bol dosiahnutý limit požiadaviek GitHubu. V časti Nastavenia → GitHub API nakonfigurujte voliteľný token alebo to skúste znova neskôr.",
|
||||||
"upstreamErrorGeneric": "Kontrola proti prúdu zlyhala: {detail}",
|
"upstreamErrorGeneric": "Kontrola proti prúdu zlyhala: {detail}",
|
||||||
"notificationsEnabled": "Upozornenia na upstream aktualizácie sú ZAPNUTÉ – kliknutím ich stlmíte",
|
"notificationsEnabled": "Upozornenia na upstream aktualizácie sú ZAPNUTÉ – kliknutím ich stlmíte",
|
||||||
"notificationsMuted": "Upstream upozornenia na aktualizácie MUTED – kliknutím aktivujete",
|
"notificationsMuted": "Upstream upozornenia na aktualizácie MUTED – kliknutím aktivujete",
|
||||||
@@ -1647,6 +1648,20 @@
|
|||||||
"saveFailed": "Nastavenie kontroly aktualizácií LXC sa nepodarilo uložiť.",
|
"saveFailed": "Nastavenie kontroly aktualizácií LXC sa nepodarilo uložiť.",
|
||||||
"purgedMessage": "Z registra bolo odstránených {count} LXC záznamov. Po opätovnom zapnutí kontroly sa doplnia pri ďalšom skenovaní."
|
"purgedMessage": "Z registra bolo odstránených {count} LXC záznamov. Po opätovnom zapnutí kontroly sa doplnia pri ďalšom skenovaní."
|
||||||
},
|
},
|
||||||
|
"githubApi": {
|
||||||
|
"title": "GitHub API",
|
||||||
|
"description": "Voliteľné overenie pre kontrolu vydaní a značiek registrovaných aplikácií.",
|
||||||
|
"configure": "Nastaviť",
|
||||||
|
"tokenLabel": "Osobný prístupový token",
|
||||||
|
"tokenPlaceholder": "github_pat_...",
|
||||||
|
"tokenHelp": "Token sa ukladá šifrovane. ProxMenux ho používa iba na požiadavky GitHub API určené na čítanie a po uložení ho už nezobrazí.",
|
||||||
|
"configured": "Token je nastavený",
|
||||||
|
"notConfigured": "Používa sa anonymná kvóta požiadaviek GitHubu",
|
||||||
|
"removeToken": "Odstrániť token",
|
||||||
|
"loadFailed": "Nastavenie GitHub API sa nepodarilo načítať.",
|
||||||
|
"saveFailed": "Token GitHub API sa nepodarilo uložiť.",
|
||||||
|
"removeFailed": "Token GitHub API sa nepodarilo odstrániť."
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notifikácie",
|
"title": "Notifikácie",
|
||||||
"description": "Nastavte kanály a filtre udalostí. Upozornenia môžete dostávať cez Telegram, Gotify, Discord alebo e-mail.",
|
"description": "Nastavte kanály a filtre udalostí. Upozornenia môžete dostávať cez Telegram, Gotify, Discord alebo e-mail.",
|
||||||
|
|||||||
@@ -1527,6 +1527,7 @@
|
|||||||
"editButton": "Redigera",
|
"editButton": "Redigera",
|
||||||
"upstreamErrorTimeout": "Nätverkstimeout vid kontakt uppströms",
|
"upstreamErrorTimeout": "Nätverkstimeout vid kontakt uppströms",
|
||||||
"upstreamErrorNetwork": "Nätverksfel: {detail}",
|
"upstreamErrorNetwork": "Nätverksfel: {detail}",
|
||||||
|
"upstreamErrorGithubRateLimit": "GitHubs förfrågningsgräns har nåtts. Konfigurera en valfri token under Inställningar → GitHub API eller försök igen senare.",
|
||||||
"upstreamErrorGeneric": "Uppströmskontroll misslyckades: {detail}",
|
"upstreamErrorGeneric": "Uppströmskontroll misslyckades: {detail}",
|
||||||
"notificationsEnabled": "Uppströmsuppdateringsmeddelanden PÅ – klicka för att stänga av ljudet",
|
"notificationsEnabled": "Uppströmsuppdateringsmeddelanden PÅ – klicka för att stänga av ljudet",
|
||||||
"notificationsMuted": "Uppströmsuppdateringsmeddelanden AVSTÄLLD – klicka för att aktivera",
|
"notificationsMuted": "Uppströmsuppdateringsmeddelanden AVSTÄLLD – klicka för att aktivera",
|
||||||
@@ -1648,6 +1649,20 @@
|
|||||||
"saveFailed": "Det gick inte att spara inställningen för upptäckt av LXC-uppdatering.",
|
"saveFailed": "Det gick inte att spara inställningen för upptäckt av LXC-uppdatering.",
|
||||||
"purgedMessage": "{count} LXC-poster har tagits bort från registret. Om detektering återaktiveras kommer de att fyllas på igen vid nästa skanningscykel."
|
"purgedMessage": "{count} LXC-poster har tagits bort från registret. Om detektering återaktiveras kommer de att fyllas på igen vid nästa skanningscykel."
|
||||||
},
|
},
|
||||||
|
"githubApi": {
|
||||||
|
"title": "GitHub API",
|
||||||
|
"description": "Valfri autentisering för kontroller av utgåvor och taggar för registrerade appar.",
|
||||||
|
"configure": "Konfigurera",
|
||||||
|
"tokenLabel": "Personlig åtkomsttoken",
|
||||||
|
"tokenPlaceholder": "github_pat_...",
|
||||||
|
"tokenHelp": "Token lagras krypterad. ProxMenux använder den endast för skrivskyddade anrop till GitHub API och visar den inte igen efter att den har sparats.",
|
||||||
|
"configured": "Token konfigurerad",
|
||||||
|
"notConfigured": "GitHubs anonyma förfrågningskvot används",
|
||||||
|
"removeToken": "Ta bort token",
|
||||||
|
"loadFailed": "Det gick inte att läsa in GitHub API-inställningen.",
|
||||||
|
"saveFailed": "Det gick inte att spara GitHub API-token.",
|
||||||
|
"removeFailed": "Det gick inte att ta bort GitHub API-token."
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Aviseringar",
|
"title": "Aviseringar",
|
||||||
"description": "Konfigurera aviseringskanaler och händelsefilter. Ta emot varningar via Telegram, Gotify, Discord eller e-post.",
|
"description": "Konfigurera aviseringskanaler och händelsefilter. Ta emot varningar via Telegram, Gotify, Discord eller e-post.",
|
||||||
|
|||||||
@@ -13747,6 +13747,54 @@ def api_apps_catalog():
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/apps/github-token', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
|
def api_apps_github_token_status():
|
||||||
|
"""Return whether the optional GitHub API token is configured.
|
||||||
|
|
||||||
|
The token itself is deliberately never returned to the client.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not notification_manager._config:
|
||||||
|
notification_manager._load_config()
|
||||||
|
value = notification_manager._config.get('github_pat', '')
|
||||||
|
return jsonify({'configured': bool(isinstance(value, str) and value.strip())})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/apps/github-token', methods=['PUT'])
|
||||||
|
@require_admin_scope
|
||||||
|
def api_apps_github_token_save():
|
||||||
|
"""Store the GitHub API token in the existing encrypted settings store."""
|
||||||
|
payload = request.get_json(silent=True) or {}
|
||||||
|
token = payload.get('token')
|
||||||
|
if not isinstance(token, str):
|
||||||
|
return jsonify({'error': 'token must be a string'}), 400
|
||||||
|
token = token.strip()
|
||||||
|
if not token:
|
||||||
|
return jsonify({'error': 'token is required'}), 400
|
||||||
|
if len(token) > 512:
|
||||||
|
return jsonify({'error': 'token exceeds the 512 character limit'}), 400
|
||||||
|
if any(ch.isspace() or ord(ch) < 33 or ord(ch) == 127 for ch in token):
|
||||||
|
return jsonify({'error': 'token contains whitespace or control characters'}), 400
|
||||||
|
|
||||||
|
result = notification_manager.save_settings({'github_pat': token})
|
||||||
|
if not result.get('success'):
|
||||||
|
return jsonify({'error': result.get('error', 'failed to save token')}), 500
|
||||||
|
return jsonify({'success': True, 'configured': True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/apps/github-token', methods=['DELETE'])
|
||||||
|
@require_admin_scope
|
||||||
|
def api_apps_github_token_remove():
|
||||||
|
"""Clear the optional GitHub API token without exposing its old value."""
|
||||||
|
result = notification_manager.save_settings({'github_pat': ''})
|
||||||
|
if not result.get('success'):
|
||||||
|
return jsonify({'error': result.get('error', 'failed to remove token')}), 500
|
||||||
|
return jsonify({'success': True, 'configured': False})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/lxc-apps/dockerhub-tag-preview', methods=['POST'])
|
@app.route('/api/lxc-apps/dockerhub-tag-preview', methods=['POST'])
|
||||||
@require_auth
|
@require_auth
|
||||||
def api_lxc_apps_dockerhub_tag_preview():
|
def api_lxc_apps_dockerhub_tag_preview():
|
||||||
|
|||||||
@@ -1285,16 +1285,13 @@ def _select_working_hint_detector(vmid, hint: dict) -> tuple[dict, Optional[str]
|
|||||||
def _github_pat() -> Optional[str]:
|
def _github_pat() -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
from notification_manager import notification_manager
|
from notification_manager import notification_manager
|
||||||
pat = notification_manager._config.get("github_pat") if notification_manager._config else None
|
# The notification manager owns the shared encrypted settings store.
|
||||||
if not pat:
|
# During very early calls its runtime cache may not have been loaded
|
||||||
return None
|
# yet, so initialise it before reading the optional GitHub token.
|
||||||
try:
|
if not notification_manager._config:
|
||||||
from notification_manager import decrypt_sensitive_value
|
notification_manager._load_config()
|
||||||
if isinstance(pat, str) and pat.startswith("encrypted:"):
|
pat = notification_manager._config.get("github_pat")
|
||||||
return decrypt_sensitive_value(pat)
|
return pat.strip() if isinstance(pat, str) and pat.strip() else None
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return pat if isinstance(pat, str) else None
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1365,7 +1362,7 @@ def _fetch_github_latest_details(config: dict) -> tuple[Optional[str], Optional[
|
|||||||
if e.code == 403:
|
if e.code == 403:
|
||||||
remaining = e.headers.get("X-RateLimit-Remaining", "1")
|
remaining = e.headers.get("X-RateLimit-Remaining", "1")
|
||||||
if remaining == "0":
|
if remaining == "0":
|
||||||
return None, "github rate limited — configure a PAT in Settings", None
|
return None, "github rate limited — configure a PAT in Settings → GitHub API", None
|
||||||
return None, "github rejected the request (403)", None
|
return None, "github rejected the request (403)", None
|
||||||
return None, f"github error {e.code}", None
|
return None, f"github error {e.code}", None
|
||||||
except (urllib.error.URLError, TimeoutError, OSError) as e:
|
except (urllib.error.URLError, TimeoutError, OSError) as e:
|
||||||
|
|||||||
@@ -4147,6 +4147,17 @@ class ProxmoxHookWatcher:
|
|||||||
'job_id': pve_job_id,
|
'job_id': pve_job_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pve_type == 'replication':
|
||||||
|
replication = self._extract_replication_context(
|
||||||
|
fields, title, message
|
||||||
|
)
|
||||||
|
data.update(replication)
|
||||||
|
entity_id = (
|
||||||
|
replication.get('job_id')
|
||||||
|
or replication.get('vmid')
|
||||||
|
or entity_id
|
||||||
|
)
|
||||||
|
|
||||||
# `system_problem` is the generic fallback of `_classify_pve` for
|
# `system_problem` is the generic fallback of `_classify_pve` for
|
||||||
# unknown/empty pve_type. Without a populated `reason`, the template
|
# unknown/empty pve_type. Without a populated `reason`, the template
|
||||||
# renders "Reason: " (empty) and `_summarize_event` falls back to
|
# renders "Reason: " (empty) and `_summarize_event` falls back to
|
||||||
@@ -4274,6 +4285,73 @@ class ProxmoxHookWatcher:
|
|||||||
|
|
||||||
self._queue.put(event)
|
self._queue.put(event)
|
||||||
return {'accepted': True, 'event_type': event_type, 'event_id': event.event_id}
|
return {'accepted': True, 'event_type': event_type, 'event_id': event.event_id}
|
||||||
|
|
||||||
|
def _extract_replication_context(self, fields: dict, title: str,
|
||||||
|
message: str) -> dict:
|
||||||
|
"""Map a native PVE replication notice to template fields."""
|
||||||
|
raw_job_id = fields.get('job-id') or fields.get('job_id') or ''
|
||||||
|
job_id = str(raw_job_id).strip()
|
||||||
|
if not re.fullmatch(r'\d+(?:-\d+)?', job_id):
|
||||||
|
combined = f'{title or ""}\n{message or ""}'
|
||||||
|
match = re.search(
|
||||||
|
r'\breplication(?:\s+job)?(?:\s*:\s*|\s+)'
|
||||||
|
r'[\'\"]?(\d+(?:-\d+)?)\b',
|
||||||
|
combined,
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
match = re.search(r'\b(\d+-\d+)\b', combined)
|
||||||
|
job_id = match.group(1) if match else ''
|
||||||
|
|
||||||
|
vmid_match = re.fullmatch(r'(\d+)(?:-\d+)?', job_id)
|
||||||
|
vmid = vmid_match.group(1) if vmid_match else ''
|
||||||
|
vmname = self._resolve_replication_guest_name(vmid)
|
||||||
|
if not vmname and vmid:
|
||||||
|
vmname = 'VM/CT'
|
||||||
|
|
||||||
|
target = str(
|
||||||
|
fields.get('job-target') or fields.get('target') or ''
|
||||||
|
).strip()
|
||||||
|
if not target:
|
||||||
|
target_match = re.search(
|
||||||
|
r'\bwith\s+target\s+[\'\"]([^\'\"\n]+)[\'\"]',
|
||||||
|
message or '',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if target_match:
|
||||||
|
target = target_match.group(1).strip()
|
||||||
|
|
||||||
|
reason_match = re.search(
|
||||||
|
r'^\s*Error:\s*(.*?)\s*\Z',
|
||||||
|
message or '',
|
||||||
|
re.IGNORECASE | re.MULTILINE | re.DOTALL,
|
||||||
|
)
|
||||||
|
reason = reason_match.group(1).strip() if reason_match else ''
|
||||||
|
if not reason:
|
||||||
|
reason = (message or title or '').strip()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'job_id': job_id,
|
||||||
|
'vmid': vmid,
|
||||||
|
'vmname': vmname,
|
||||||
|
'target_node': target,
|
||||||
|
'reason': reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_replication_guest_name(vmid: str) -> str:
|
||||||
|
"""Resolve the replicated guest name from the cluster config."""
|
||||||
|
if not vmid or not vmid.isdigit():
|
||||||
|
return ''
|
||||||
|
for base in ('/etc/pve/qemu-server', '/etc/pve/lxc'):
|
||||||
|
try:
|
||||||
|
with open(f'{base}/{vmid}.conf', encoding='utf-8') as config:
|
||||||
|
for line in config:
|
||||||
|
if line.startswith(('name:', 'hostname:')):
|
||||||
|
return line.split(':', 1)[1].strip()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return ''
|
||||||
|
|
||||||
def _classify_pve(self, pve_type: str, severity: str,
|
def _classify_pve(self, pve_type: str, severity: str,
|
||||||
title: str, message: str) -> tuple:
|
title: str, message: str) -> tuple:
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ ENCRYPTION_KEY_FILE = Path('/usr/local/share/proxmenux/.notification_key')
|
|||||||
|
|
||||||
# Keys that contain sensitive data and should be encrypted
|
# Keys that contain sensitive data and should be encrypted
|
||||||
SENSITIVE_KEYS = {
|
SENSITIVE_KEYS = {
|
||||||
|
# Optional GitHub API token used by the LXC app version tracker.
|
||||||
|
# It lives in the shared settings store so it benefits from the same
|
||||||
|
# ENC2 encryption and never needs a second secrets file.
|
||||||
|
'github_pat',
|
||||||
'ai_api_key', # Legacy - kept for migration
|
'ai_api_key', # Legacy - kept for migration
|
||||||
'ai_api_key_groq',
|
'ai_api_key_groq',
|
||||||
'ai_api_key_gemini',
|
'ai_api_key_gemini',
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from queue import Queue
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
if str(SCRIPTS_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||||
|
|
||||||
|
import notification_events # noqa: E402
|
||||||
|
import notification_templates # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class ReplicationWebhookTests(unittest.TestCase):
|
||||||
|
def _process(self, payload, guest_name='fileserver'):
|
||||||
|
watcher = notification_events.ProxmoxHookWatcher(Queue())
|
||||||
|
with mock.patch.object(
|
||||||
|
watcher,
|
||||||
|
'_resolve_replication_guest_name',
|
||||||
|
return_value=guest_name,
|
||||||
|
), mock.patch.object(
|
||||||
|
notification_events,
|
||||||
|
'capture_journal_context',
|
||||||
|
return_value='',
|
||||||
|
):
|
||||||
|
result = watcher.process_webhook(payload)
|
||||||
|
return result, watcher._queue.get_nowait()
|
||||||
|
|
||||||
|
def test_structured_job_id_populates_template_fields(self):
|
||||||
|
reason = 'command zfs error: cannot open pool\nremote side unavailable'
|
||||||
|
result, event = self._process({
|
||||||
|
'title': "Replication Job: '100-0' failed",
|
||||||
|
'message': (
|
||||||
|
"Replication job '100-0' with target 'pve02' and schedule "
|
||||||
|
"'*/15' failed!\n\n"
|
||||||
|
"Last successful sync: 2026-09-02 15:00:00\n"
|
||||||
|
"Next sync try: 2026-09-02 15:30:00\n"
|
||||||
|
"Failure count: 1\n\n"
|
||||||
|
f"Error:\n{reason}"
|
||||||
|
),
|
||||||
|
'severity': 'error',
|
||||||
|
'fields': {
|
||||||
|
'type': 'replication',
|
||||||
|
'hostname': 'pve01',
|
||||||
|
'job-id': '100-0',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertTrue(result['accepted'])
|
||||||
|
self.assertEqual(event.event_type, 'replication_fail')
|
||||||
|
self.assertEqual(event.entity_id, '100-0')
|
||||||
|
self.assertEqual(event.data['job_id'], '100-0')
|
||||||
|
self.assertEqual(event.data['vmid'], '100')
|
||||||
|
self.assertEqual(event.data['vmname'], 'fileserver')
|
||||||
|
self.assertEqual(event.data['target_node'], 'pve02')
|
||||||
|
self.assertEqual(event.data['reason'], reason)
|
||||||
|
|
||||||
|
rendered = notification_templates.render_template(
|
||||||
|
event.event_type,
|
||||||
|
event.data,
|
||||||
|
)
|
||||||
|
self.assertIn('fileserver (100)', rendered['title'])
|
||||||
|
self.assertIn('ID: 100', rendered['body_text'])
|
||||||
|
self.assertIn(reason, rendered['body_text'])
|
||||||
|
|
||||||
|
def test_title_and_message_are_used_when_job_id_field_is_missing(self):
|
||||||
|
_, event = self._process({
|
||||||
|
'title': "Replication Job: '212-3' failed",
|
||||||
|
'message': (
|
||||||
|
"Replication job '212-3' with target 'pve03' failed!\n\n"
|
||||||
|
"Error: storage 'replica-zfs' is not available"
|
||||||
|
),
|
||||||
|
'severity': 'error',
|
||||||
|
'fields': {'type': 'replication', 'hostname': 'pve01'},
|
||||||
|
}, guest_name='')
|
||||||
|
|
||||||
|
self.assertEqual(event.entity_id, '212-3')
|
||||||
|
self.assertEqual(event.data['vmid'], '212')
|
||||||
|
self.assertEqual(event.data['vmname'], 'VM/CT')
|
||||||
|
self.assertEqual(event.data['target_node'], 'pve03')
|
||||||
|
self.assertEqual(
|
||||||
|
event.data['reason'],
|
||||||
|
"storage 'replica-zfs' is not available",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_error_block_never_renders_an_empty_reason(self):
|
||||||
|
message = "Replication job '300-0' failed unexpectedly"
|
||||||
|
_, event = self._process({
|
||||||
|
'title': "Replication Job: '300-0' failed",
|
||||||
|
'message': message,
|
||||||
|
'severity': 'error',
|
||||||
|
'fields': {'type': 'replication', 'hostname': 'pve01'},
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(event.data['reason'], message)
|
||||||
|
rendered = notification_templates.render_template(
|
||||||
|
event.event_type,
|
||||||
|
event.data,
|
||||||
|
)
|
||||||
|
self.assertIn(f'Reason: {message}', rendered['body_text'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user