mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
i18n: localize stopped scheduled update reason
This commit is contained in:
@@ -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,9 @@ 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"
|
||||
? t("vmLxc.scheduled.reasonStoppedContainer")
|
||||
: scheduleLastRunReason
|
||||
return (
|
||||
<div className={`space-y-1.5 ${spacing}`}>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
@@ -2448,9 +2470,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
</span></>
|
||||
)}
|
||||
</div>
|
||||
{scheduleLastRunReason && (
|
||||
{scheduleLastRunReasonText && (
|
||||
<div className="text-xs text-muted-foreground break-words">
|
||||
{scheduleLastRunReason}
|
||||
{scheduleLastRunReasonText}
|
||||
</div>
|
||||
)}
|
||||
{scheduleLastRunRebootRequired && (
|
||||
|
||||
@@ -1288,6 +1288,7 @@
|
||||
"runFailed": "✗ failed",
|
||||
"runDeferred": "held for safety",
|
||||
"runSkipped": "nothing pending",
|
||||
"reasonStoppedContainer": "Container is stopped; scheduled update was skipped.",
|
||||
"releaseDelaySummary": "{days}-day release hold",
|
||||
"releaseDelayLabel": "Wait after a new app release",
|
||||
"releaseDelayNone": "No waiting period",
|
||||
|
||||
@@ -1288,6 +1288,7 @@
|
||||
"runFailed": "✗ zlyhalo",
|
||||
"runDeferred": "odložené z bezpečnostných dôvodov",
|
||||
"runSkipped": "nič nečaká",
|
||||
"reasonStoppedContainer": "Kontajner je vypnutý; plánovaná aktualizácia sa preskočila.",
|
||||
"releaseDelaySummary": "odklad {days} deň/dní",
|
||||
"releaseDelayLabel": "Počkať po novej verzii aplikácie",
|
||||
"releaseDelayNone": "Bez čakacej doby",
|
||||
|
||||
@@ -22700,6 +22700,7 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
requested_target = sched.get("target") or "both"
|
||||
deferred_targets: list[str] = []
|
||||
reasons: list[str] = []
|
||||
reason_code: str | None = None
|
||||
reboot_required: bool | None = None
|
||||
reboot_packages: list[str] = []
|
||||
reboot_check_error: str | None = None
|
||||
@@ -22751,6 +22752,7 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
'status': status,
|
||||
'target': actual_target,
|
||||
'reason': reason,
|
||||
'reason_code': reason_code,
|
||||
'run_id': run_id,
|
||||
'requested_targets': requested_targets,
|
||||
'executed_targets': list(executed),
|
||||
@@ -22763,6 +22765,7 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
}
|
||||
|
||||
if not originally_running:
|
||||
reason_code = 'stopped_container'
|
||||
reasons.append('container is stopped; scheduled update was skipped')
|
||||
return finish('skipped', requested_target, [])
|
||||
|
||||
@@ -22978,6 +22981,7 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
|
||||
timeout=60 * 60,
|
||||
)
|
||||
if _scheduled_runner_reported_stopped(r, log_path, scheduled_stopped_marker):
|
||||
reason_code = 'stopped_container'
|
||||
reasons.append('container was stopped before scheduled update could begin')
|
||||
return finish('skipped', target, [])
|
||||
reboot_required, reboot_packages, reboot_check_error = (
|
||||
@@ -23055,12 +23059,14 @@ def _scheduler_loop():
|
||||
status = outcome.get('status') or 'failure'
|
||||
actual_target = outcome.get('target') or 'both'
|
||||
reason = outcome.get('reason')
|
||||
reason_code = outcome.get('reason_code')
|
||||
try:
|
||||
lxc_apps.record_schedule_run(
|
||||
_vmid,
|
||||
status,
|
||||
actual_target,
|
||||
reason,
|
||||
reason_code=reason_code,
|
||||
log_name=outcome.get('log_name'),
|
||||
reboot_required=outcome.get('reboot_required'),
|
||||
reboot_packages=outcome.get('reboot_packages'),
|
||||
|
||||
@@ -94,6 +94,7 @@ _VALID_UPSTREAM_TYPES = ("github", "http_json", "docker_hub")
|
||||
# `*/N` step, and comma lists — that covers every preset the UI
|
||||
# exposes and the freeform "custom" text field.
|
||||
_VALID_SCHEDULE_TARGETS = ("os", "app", "both")
|
||||
_SCHEDULE_REASON_CODES = frozenset({"stopped_container"})
|
||||
_SCHEDULE_TARGET_ID_RE = re.compile(
|
||||
r"^(?:os|apps|oci_image|app:[A-Za-z0-9_-]{1,64}|docker-engine|docker-(?:compose|container):[A-Za-z0-9][A-Za-z0-9_.-]{0,127}|docker-unit:[a-f0-9]{20})$"
|
||||
)
|
||||
@@ -3918,6 +3919,7 @@ def record_schedule_run(
|
||||
target: str,
|
||||
reason: Optional[str] = None,
|
||||
*,
|
||||
reason_code: Optional[str] = None,
|
||||
log_name: Optional[str] = None,
|
||||
reboot_required: Optional[bool] = None,
|
||||
reboot_packages: Optional[list[str]] = None,
|
||||
@@ -3937,6 +3939,10 @@ def record_schedule_run(
|
||||
sidecar["schedule"]["last_run_reason"] = str(reason)[:300]
|
||||
else:
|
||||
sidecar["schedule"].pop("last_run_reason", None)
|
||||
if reason_code in _SCHEDULE_REASON_CODES:
|
||||
sidecar["schedule"]["last_run_reason_code"] = reason_code
|
||||
else:
|
||||
sidecar["schedule"].pop("last_run_reason_code", None)
|
||||
if log_name:
|
||||
sidecar["schedule"]["last_run_log"] = os.path.basename(str(log_name))[:220]
|
||||
else:
|
||||
|
||||
@@ -33,6 +33,8 @@ class ScheduledUpdateRecordTests(unittest.TestCase):
|
||||
self.vmid,
|
||||
'success',
|
||||
'both',
|
||||
reason='container is stopped; scheduled update was skipped',
|
||||
reason_code='stopped_container',
|
||||
log_name='../9911-scheduled-' + ('a' * 32) + '.log',
|
||||
reboot_required=True,
|
||||
reboot_packages=['linux-image-amd64', 'libc6'],
|
||||
@@ -43,11 +45,31 @@ class ScheduledUpdateRecordTests(unittest.TestCase):
|
||||
'9911-scheduled-' + ('a' * 32) + '.log',
|
||||
)
|
||||
self.assertTrue(schedule['last_run_reboot_required'])
|
||||
self.assertEqual(schedule['last_run_reason_code'], 'stopped_container')
|
||||
self.assertEqual(
|
||||
schedule['last_run_reboot_packages'],
|
||||
['linux-image-amd64', 'libc6'],
|
||||
)
|
||||
|
||||
def test_record_rejects_unknown_reason_code_and_clears_the_previous_one(self):
|
||||
self.assertTrue(lxc_apps.record_schedule_run(
|
||||
self.vmid,
|
||||
'skipped',
|
||||
'os',
|
||||
reason='container is stopped; scheduled update was skipped',
|
||||
reason_code='stopped_container',
|
||||
))
|
||||
self.assertTrue(lxc_apps.record_schedule_run(
|
||||
self.vmid,
|
||||
'failure',
|
||||
'os',
|
||||
reason='update runner exited with code 9',
|
||||
reason_code='untrusted_value',
|
||||
))
|
||||
schedule = lxc_apps._read_sidecar(self.vmid)['schedule']
|
||||
self.assertEqual(schedule['last_run_reason'], 'update runner exited with code 9')
|
||||
self.assertNotIn('last_run_reason_code', schedule)
|
||||
|
||||
def test_lifecycle_clear_preserves_run_and_log(self):
|
||||
lxc_apps.record_schedule_run(
|
||||
self.vmid,
|
||||
|
||||
Reference in New Issue
Block a user