Merge remote-tracking branch 'origin/develop' into HEAD

# Conflicts:
#	.github/scripts/tests/test_command_descriptions.py
This commit is contained in:
MacRimi
2026-10-16 23:32:05 +02:00
76 changed files with 3714 additions and 309 deletions
@@ -0,0 +1,32 @@
"use client"
import { ExternalLink } from "lucide-react"
import { useT } from "../lib/i18n/provider"
// These are examples, never defaults: opening help must not replace a saved
// destination or put placeholder credentials into the notification settings.
export function AppriseServiceHelp() {
const t = useT()
return <details className="rounded-md border border-border bg-muted/20 p-2 text-xs">
<summary className="cursor-pointer font-medium">{t("settings.notifications.ui.appriseServiceHelp")}</summary>
<div className="grid gap-3 pt-3 sm:grid-cols-2">
<div className="space-y-1.5 min-w-0">
<h4 className="font-medium">Slack</h4>
<p className="text-muted-foreground">{t("settings.notifications.ui.appriseSlackSteps")}</p>
<code className="block break-all rounded bg-background p-2 text-[10px]">https://hooks.slack.com/services/TOKEN_A/TOKEN_B/TOKEN_C</code>
<a href="https://appriseit.com/services/slack/" target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-cyan-500 hover:underline">
{t("settings.notifications.ui.appriseSlackDocs")} <ExternalLink className="h-3 w-3" />
</a>
</div>
<div className="space-y-1.5 min-w-0">
<h4 className="font-medium">Matrix</h4>
<p className="text-muted-foreground">{t("settings.notifications.ui.appriseMatrixSteps")}</p>
<code className="block break-all rounded bg-background p-2 text-[10px]">matrixs://USER:PASSWORD@matrix.example.com/!ROOM_ID:matrix.example.com</code>
<a href="https://appriseit.com/services/matrix/" target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-cyan-500 hover:underline">
{t("settings.notifications.ui.appriseMatrixDocs")} <ExternalLink className="h-3 w-3" />
</a>
</div>
</div>
<p className="mt-3 text-muted-foreground">{t("settings.notifications.ui.appriseServiceFinish")}</p>
</details>
}
@@ -1,5 +1,6 @@
"use client"
import { AppriseServiceHelp } from "./apprise-service-help"
import { useState, useEffect, useCallback } from "react"
import { useTheme } from "next-themes"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
@@ -2061,6 +2062,7 @@ export function NotificationSettings() {
Enable toggle + Test button pattern as the other
channels. */}
<TabsContent value="apprise" className="space-y-3 pt-2">
<AppriseServiceHelp />
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Label className="text-xs font-medium">{t("settings.notifications.ui.enableApprise")}</Label>
+27 -3
View File
@@ -35,6 +35,23 @@ import DOMPurify from "dompurify"
import { marked } from "marked"
import { getCountFormKey, useI18n, useT } from "@/lib/i18n/provider"
// A schedule record predating the structured reason code still contains one
// of these exact diagnostic messages. Recognise only those known historical
// values so an already-recorded safe skip localizes after an upgrade, while
// unrelated freeform diagnostics remain verbatim for troubleshooting.
const LEGACY_STOPPED_SCHEDULE_REASONS = new Set([
"container is stopped; scheduled update was skipped",
"container was stopped before scheduled update could begin",
])
const scheduleReasonCodeFromPersisted = (reasonCode: unknown, reason: unknown): string | null => {
if (typeof reasonCode === "string" && reasonCode) return reasonCode
if (typeof reason === "string" && LEGACY_STOPPED_SCHEDULE_REASONS.has(reason.toLowerCase())) {
return "stopped_container"
}
return null
}
// Sent by /api/vms only for LXC rows, only when the user has enabled
// `lxc_updates_available` notifications. The Monitor populates this
// from managed_installs registry → frontend uses it to render the
@@ -2077,6 +2094,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
const [scheduleLastRunAt, setScheduleLastRunAt] = useState<string | null>(null)
const [scheduleLastRunStatus, setScheduleLastRunStatus] = useState<string | null>(null)
const [scheduleLastRunReason, setScheduleLastRunReason] = useState<string | null>(null)
const [scheduleLastRunReasonCode, setScheduleLastRunReasonCode] = useState<string | null>(null)
const [scheduleLastRunLog, setScheduleLastRunLog] = useState<string | null>(null)
const [scheduleLastRunRebootRequired, setScheduleLastRunRebootRequired] = useState(false)
const [scheduleLastRunRebootPackages, setScheduleLastRunRebootPackages] = useState<string[]>([])
@@ -2153,6 +2171,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
setScheduleLastRunAt(s.last_run_at || null)
setScheduleLastRunStatus(s.last_run_status || null)
setScheduleLastRunReason(s.last_run_reason || null)
setScheduleLastRunReasonCode(scheduleReasonCodeFromPersisted(s.last_run_reason_code, s.last_run_reason))
setScheduleLastRunLog(s.last_run_log || null)
setScheduleLastRunRebootRequired(s.last_run_reboot_required === true)
setScheduleLastRunRebootPackages(Array.isArray(s.last_run_reboot_packages)
@@ -2430,6 +2449,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
const renderScheduleRunDetails = (indented = false) => {
if (!scheduleLastRunAt || !selectedVM) return null
const spacing = indented ? "pl-4" : ""
const scheduleLastRunReasonText = scheduleLastRunReasonCode === "stopped_container"
? selectedVM.status === "stopped"
? t("vmLxc.scheduled.reasonStoppedContainer")
: t("vmLxc.scheduled.reasonStoppedContainerHistorical")
: scheduleLastRunReason
return (
<div className={`space-y-1.5 ${spacing}`}>
<div className="text-xs text-muted-foreground">
@@ -2443,14 +2467,14 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
: scheduleLastRunStatus === "deferred"
? t("vmLxc.scheduled.runDeferred")
: scheduleLastRunStatus === "skipped"
? t("vmLxc.scheduled.runSkipped")
? t("vmLxc.scheduled.runSkippedStatus")
: t("vmLxc.scheduled.runFailed")}
</span></>
)}
</div>
{scheduleLastRunReason && (
{scheduleLastRunReasonText && (
<div className="text-xs text-muted-foreground break-words">
{scheduleLastRunReason}
{scheduleLastRunReasonText}
</div>
)}
{scheduleLastRunRebootRequired && (