ProxMenux 1.2.6.2-beta: OCI containers in the Monitor, docs and fixes

OCI manager Apps
- App tab: containers installed from an OCI image are identified from their
  installation record; the application and image versions are shown and an
  update is detected by image digest; repository link; Refresh data.
- Updates tab for OCI containers: Update and Recreate run the same flow as the
  OCI menu in the Monitor terminal; the pre-update backup can be kept in a
  backup storage; scheduled image updates with an optional minimum age.
- Logs tab: console output of the application, kept on the host
  (lxc.console.logfile + logrotate) and followed live.
- The Proxmox console opens a shell (cmode: shell) when the image has one.
- A damaged image download is fetched again before failing.
- Multi-container applications open at their LAN address; volume mount
  points on block storage report their usage.

Monitor
- Proxmox notifications are delivered to a loopback-only HTTP listener when
  HTTPS is enabled, so they no longer fail certificate verification.
- Log persistence counts recurring patterns only; an ended burst is not
  reported as persistent and its warning clears on its own (#386).
- Proxmox notification config backups are deduplicated and capped at three.
- The update icon on the Apps page opens the container on its Updates tab.
- Version 1.2.6.2-beta and its release notes in every Monitor language.

Docs
- OCI manager Apps and Audit & Report rebuilt as per-page message files,
  with a new page for OCI containers in the Monitor.
- Seven pages fixed where rich-text tags were missing from t.rich.

Translations
- Spanish fixes across the OCI engine, the Monitor and the TUI menus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-09-25 21:51:12 +02:00
co-authored by Claude Opus 5.5
parent 386d33df6e
commit 4437a671d2
524 changed files with 14459 additions and 3841 deletions
+407 -11
View File
@@ -21,7 +21,8 @@ import useSWR from "swr"
import { MetricsView } from "./metrics-dialog"
import { LxcTerminalModal } from "./lxc-terminal-modal"
import { ScriptTerminalModal } from "./script-terminal-modal"
import { LxcAppPanel, ThemeAwareLogo } from "./lxc-app-panel"
import { LxcAppPanel, ThemeAwareLogo, ociImageLabel } from "./lxc-app-panel"
import { OciConsoleLogPanel } from "./oci-console-log-panel"
import { AppUpdaterEditor, type AppUpdateMethod } from "./app-updater-editor"
import { formatStorage } from "../lib/utils"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
@@ -40,6 +41,17 @@ interface LxcPackageUpdate {
latest: string
security: boolean
}
// A container installed by OCI manager Apps, read from its record.
interface OciInstanceInfo {
oci_instance: boolean
console_log: boolean
stack: boolean
primary_vmid: number
members: number[]
host_directories: boolean
pending: boolean
}
interface LxcUpdateCheck {
available: boolean
count: number
@@ -114,6 +126,12 @@ interface LxcAppWatch {
error: string | null
checked_at: string | null
has_repo?: boolean
// OCI image of an application installed by OCI manager Apps.
image_reference?: string | null
image_created?: string | null
installed_digest?: string | null
latest_image_created?: string | null
latest_digest?: string | null
// Set for the synthetic entry that represents a ProxMenux-managed
// OCI app (Secure Gateway). The frontend renders it read-only +
// wires the Update action to /api/oci/installed/<id>/update.
@@ -951,7 +969,15 @@ export function VirtualMachines() {
const [backupPbsChangeMode, setBackupPbsChangeMode] = useState<string>("default")
// Tab state for modal
const [activeModalTab, setActiveModalTab] = useState<"status" | "mounts" | "backups" | "app" | "updates" | "firewall">("status")
const [activeModalTab, setActiveModalTab] = useState<"status" | "mounts" | "backups" | "app" | "updates" | "firewall" | "logs">("status")
// OCI containers keep their console output on the host; the Logs tab is
// offered only when the backend finds that log for the open container.
const [consoleLogAvailable, setConsoleLogAvailable] = useState(false)
// Set when the open container was installed by OCI manager Apps: its
// Updates tab replaces the image instead of updating packages.
const [ociInstance, setOciInstance] = useState<OciInstanceInfo | null>(null)
const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "recreate" } | null>(null)
const [scheduleAckExternal, setScheduleAckExternal] = useState(false)
// Firewall log state — fetched only when the operator opens that tab
// so a CT/VM without firewall use doesn't pay the pvesh cost on every
@@ -1094,9 +1120,10 @@ export function VirtualMachines() {
if (!vm) return
handleVMClick(vm)
// handleVMClick resets the inner tab to "status"; override to
// "app" in the same render tick — React batches these and the
// the requested tab ("app" by default, "updates" from the update
// icon) in the same render tick — React batches these and the
// last setActiveModalTab wins.
setActiveModalTab("app")
setActiveModalTab(detail.tab === "updates" ? "updates" : "app")
}
window.addEventListener("openLxcAppModal", handler as EventListener)
return () => window.removeEventListener("openLxcAppModal", handler as EventListener)
@@ -1249,6 +1276,8 @@ export function VirtualMachines() {
setFirewallLogs([])
setFirewallLogError(null)
setFirewallEnabled(true)
setConsoleLogAvailable(false)
setOciInstance(null)
// Prime UI from last-known payloads so a reopened guest never
// flashes "Loading…" — the backend and this cache both revalidate
@@ -1290,6 +1319,7 @@ export function VirtualMachines() {
// extends the same idea to the two tabs still on lazy-fetch.
if (vm.type === "lxc") {
fetchMountPoints(vm.vmid)
fetchOciInstance(vm.vmid)
// Shared cache dedup: if a hover already fired this, we share
// the same promise (no duplicate backend work). The App panel
// reads from the same cache, so switching to that tab either
@@ -1403,6 +1433,17 @@ export function VirtualMachines() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vmidsKey])
const fetchOciInstance = (vmid: number) =>
fetchApi<OciInstanceInfo>(`/api/lxc/${vmid}/oci-instance`)
.then((r) => {
setConsoleLogAvailable(!!r?.console_log)
setOciInstance(r?.oci_instance ? r : null)
})
.catch(() => {
setConsoleLogAvailable(false)
setOciInstance(null)
})
const fetchMountPoints = async (vmid: number) => {
// Two fetches in parallel:
// 1) STATIC — configured mp entries + PVE classification. Backed
@@ -1993,6 +2034,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
? s.targets.map((value: any) => String(value))
: ([...(legacyTarget !== "app" ? ["os"] : []), ...(legacyTarget !== "os" ? ["apps"] : [])]))
setScheduleReleaseDelayDays(Number.isInteger(Number(s.release_delay_days)) ? Number(s.release_delay_days) : 0)
setScheduleAckExternal(s.acknowledge_external_data === true)
if (s.backup !== undefined) setApplyBackup(!!s.backup)
if (s.backup_storage) setApplyBackupStorage(s.backup_storage)
if (s.restart !== undefined) setApplyRestart(!!s.restart)
@@ -2144,8 +2186,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
// leave scheduleConfigured=false and Save PUTs cron="" so the
// backend doesn't resurrect the schedule.
const cronToSave = scheduleEnabled || scheduleConfigured ? scheduleCron : ""
const hasOsTarget = scheduleTargets.includes("os")
const hasAppTarget = scheduleTargets.some((value) => value !== "os")
// A container installed by OCI manager Apps has one thing to update on
// a schedule: its image.
const targetsToSave = ociInstance ? ["oci_image"] : scheduleTargets
const hasOsTarget = targetsToSave.includes("os")
const hasAppTarget = targetsToSave.some((value) => value !== "os")
const derivedTarget: "os" | "app" | "both" = hasOsTarget && hasAppTarget ? "both" : hasOsTarget ? "os" : "app"
try {
await fetchApi(`/api/vms/${vmid}/schedule`, {
@@ -2155,11 +2200,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
enabled: scheduleEnabled,
cron: cronToSave,
target: derivedTarget,
targets: scheduleTargets,
targets: targetsToSave,
release_delay_days: scheduleReleaseDelayDays,
backup: applyBackup,
backup_storage: applyBackupStorage || selectedBackupStorage || "",
restart: applyRestart,
restart: ociInstance ? false : applyRestart,
acknowledge_external_data: ociInstance ? scheduleAckExternal : false,
}),
})
if (cronToSave.trim()) setScheduleConfigured(true)
@@ -2181,6 +2227,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
scheduleTarget: scheduleTarget,
scheduleTargets: [...scheduleTargets],
scheduleReleaseDelayDays: scheduleReleaseDelayDays,
scheduleAckExternal: scheduleAckExternal,
})
setOptionsEditMode(true)
}
@@ -2195,6 +2242,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
setScheduleTarget(optionsSnapshot.scheduleTarget)
setScheduleTargets(optionsSnapshot.scheduleTargets || ["os", "apps"])
setScheduleReleaseDelayDays(optionsSnapshot.scheduleReleaseDelayDays)
setScheduleAckExternal(!!optionsSnapshot.scheduleAckExternal)
}
setOptionsSnapshot(null)
setOptionsEditMode(false)
@@ -3761,6 +3809,24 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
</Badge>
</button>
)}
{/* Logs tab — native OCI containers only: the console
output of the image's main process, read live from
the host on every open. */}
{selectedVM?.type === "lxc" && (consoleLogAvailable || selectedVM?.update_check?.is_oci_lxc) && (
<button
onClick={() => setActiveModalTab("logs")}
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap shrink-0 ${
activeModalTab === "logs"
? "border-sky-500 text-sky-500"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<FileText className="h-4 w-4" />
<span className={activeModalTab === "logs" ? "" : "hidden sm:inline"}>
{t("vmLxc.tabs.logs")}
</span>
</button>
)}
<button
onClick={() => setActiveModalTab("backups")}
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap shrink-0 ${
@@ -5130,8 +5196,299 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
)
})()}
{/* Branch 1 — OCI-image container */}
{!selectedVM.update_check?.managed_oci_app &&
{/* Branch 0b — container installed by OCI manager Apps. It is
updated by replacing its image, through the same flow as
the OCI menu; nothing of the package or app updaters of an
ordinary LXC applies. */}
{!selectedVM.update_check?.managed_oci_app && ociInstance && (() => {
const ociApp = (selectedVM.app_watches || []).find((a) => a.installed_via === "oci_image")
const hasUpdate = ociApp?.update_available === true
const upToDate = ociApp?.update_available === false
const installedLabel = ociApp ? ociImageLabel(ociApp.image_created, ociApp.installed_digest) : ""
const latestLabel = ociApp ? ociImageLabel(ociApp.latest_image_created, ociApp.latest_digest) : ""
const updateBtnCls = hasUpdate
? "bg-purple-600/15 hover:bg-purple-600/25 border border-purple-500/40 text-purple-300 hover:text-purple-200"
: "bg-green-500/10 hover:bg-green-500/20 border border-green-500/30 text-green-400 hover:text-green-300"
const neutralBtnCls = "border border-input bg-background text-foreground/80 hover:bg-accent hover:text-accent-foreground"
const storageForBackup = applyBackupStorage || selectedBackupStorage
return (
<>
<Card className="border border-border bg-card/50">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3 min-w-0">
<Package className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<h3 className="text-sm font-semibold text-foreground truncate">
{ociApp?.name || selectedVM.name}
</h3>
</div>
{ociApp?.checked_at && (
<div className="text-xs text-muted-foreground mb-3 leading-relaxed">
{t("vmLxc.updates.lastCheckedPrefix")} {new Date(ociApp.checked_at).toLocaleString()}
</div>
)}
{!ociApp ? (
<div className="text-sm text-muted-foreground">{t("vmLxc.ociUpdates.notTracked")}</div>
) : hasUpdate ? (
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<Package className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<span>{t("vmLxc.ociUpdates.installedImage")} <code className="text-foreground/80">{installedLabel}</code></span>
</div>
<div className="flex items-center gap-2">
<ArrowUpCircle className="h-4 w-4 text-purple-400 flex-shrink-0" />
<span>{t("vmLxc.ociUpdates.newImage", { image: latestLabel })}</span>
</div>
</div>
) : upToDate ? (
<div className="text-sm text-muted-foreground flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" />
<span>{t("vmLxc.updates.upToDateAtLabel")} <code className="text-foreground/80">{installedLabel}</code></span>
</div>
) : (
<div className="text-sm text-muted-foreground flex items-center gap-2">
<Package className="h-4 w-4 flex-shrink-0" />
<span>{t("vmLxc.ociUpdates.installedImage")} <code className="text-foreground/80">{installedLabel || "—"}</code></span>
</div>
)}
{ociInstance.stack && (
<div className="text-xs text-muted-foreground mt-3 leading-relaxed">
{t("vmLxc.ociUpdates.stackNote", { primary: ociInstance.primary_vmid, count: ociInstance.members.length })}
</div>
)}
{ociInstance.pending && (
<div className="text-xs text-amber-400 mt-3 leading-relaxed">{t("vmLxc.ociUpdates.pendingNote")}</div>
)}
<div className="mt-4 pt-4 border-t border-border/50 flex flex-wrap justify-end gap-2">
{!ociInstance.stack && !ociInstance.pending && (
<Button
size="sm"
className={neutralBtnCls}
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "recreate" })}
>
<RotateCcw className="h-4 w-4 mr-1.5" />
{t("vmLxc.ociUpdates.recreate")}
</Button>
)}
<Button
size="sm"
className={updateBtnCls}
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "update" })}
>
{hasUpdate
? <ArrowUpCircle className="h-4 w-4 mr-1.5" />
: <RefreshCw className="h-4 w-4 mr-1.5" />}
{ociInstance.pending ? t("vmLxc.ociUpdates.recover") : t("vmLxc.ociUpdates.update")}
</Button>
</div>
</CardContent>
</Card>
{/* Options — the backup the update keeps and the
scheduled image update. */}
<Card className={optionsEditMode ? "border border-border bg-card" : "border border-border bg-card/50"}>
<CardContent className="p-4 space-y-4">
<h3 className="text-sm font-semibold text-foreground">{t("vmLxc.options.title")}</h3>
{!optionsEditMode ? (
<div className="text-sm flex items-center gap-2">
{applyBackup ? (
<CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" />
) : (
<div className="h-4 w-4 rounded-full border border-muted-foreground/40 flex-shrink-0" />
)}
<span>
{applyBackup
? <>{t("vmLxc.ociUpdates.keepBackup")} <span className="text-muted-foreground">{t("vmLxc.options.snapshotOn", { storage: storageForBackup || t("vmLxc.options.storageAuto") })}</span></>
: <span className="text-muted-foreground">{t("vmLxc.ociUpdates.noKeepBackup")}</span>}
</span>
</div>
) : (
<div className="space-y-3">
<div className="flex items-start gap-2 text-sm">
<Checkbox
id="oci-keep-backup"
checked={applyBackup}
onCheckedChange={(v) => setApplyBackup(Boolean(v))}
className="mt-0.5"
/>
<Label htmlFor="oci-keep-backup" className="leading-tight cursor-pointer">
<span>{t("vmLxc.ociUpdates.keepBackup")}</span>
<div className="text-xs text-muted-foreground mt-0.5">{t("vmLxc.ociUpdates.keepBackupHelp")}</div>
</Label>
</div>
{applyBackup && backupStorages.length > 0 && (
<div className="pl-6">
<Label className="text-xs text-muted-foreground">{t("vmLxc.options.backupStorage")}</Label>
<Select value={storageForBackup} onValueChange={setApplyBackupStorage}>
<SelectTrigger className="h-8 text-sm mt-1 max-w-xs">
<SelectValue placeholder={t("vmLxc.options.pickStorage")} />
</SelectTrigger>
<SelectContent>
{backupStorages.map((st) => (
<SelectItem key={st.storage} value={st.storage}>{st.storage}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
)}
<div className="pt-4 border-t border-border/50 space-y-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-foreground">{t("vmLxc.scheduled.title")}</div>
{optionsEditMode && (
<div className="text-xs text-muted-foreground mt-0.5">{t("vmLxc.ociUpdates.scheduleHelp")}</div>
)}
</div>
<Switch
checked={scheduleEnabled}
onCheckedChange={(v) => { if (optionsEditMode) setScheduleEnabled(v) }}
disabled={!optionsEditMode}
className="data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-input border border-border"
/>
</div>
{!optionsEditMode && scheduleConfigured && (
<div className="text-sm space-y-1.5">
<div className="flex items-center gap-2 flex-wrap">
<span className={"h-2 w-2 rounded-full flex-shrink-0 " + (scheduleEnabled ? "bg-green-500" : "bg-muted-foreground/40")} />
<span className={scheduleEnabled ? "" : "text-muted-foreground"}>
<span className="text-foreground/80">{t("vmLxc.scheduled.chipLabel")}</span> — {humanCron(scheduleCron)}
{!scheduleEnabled && <span className="text-muted-foreground"> {t("vmLxc.scheduled.disabledSuffix")}</span>}
</span>
</div>
{scheduleReleaseDelayDays > 0 && (
<div className="text-xs text-muted-foreground pl-4">
{t("vmLxc.scheduled.releaseDelaySummary", { days: scheduleReleaseDelayDays })}
</div>
)}
{renderScheduleRunDetails(true)}
</div>
)}
{!optionsEditMode && !scheduleConfigured && (
<div className="text-sm flex items-center gap-2 text-muted-foreground">
<div className="h-4 w-4 rounded-full border border-muted-foreground/40 flex-shrink-0" />
<span>{t("vmLxc.scheduled.notScheduled")}</span>
</div>
)}
{optionsEditMode && scheduleEnabled && (
<div className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<Label className="text-xs text-muted-foreground">{t("vmLxc.scheduled.frequency")}</Label>
<Select
value={schedulePreset}
onValueChange={(v) => {
setSchedulePreset(v)
const preset = CRON_PRESETS.find((p) => p.value === v)
if (preset && preset.cron) setScheduleCron(preset.cron)
}}
>
<SelectTrigger className="h-8 text-sm mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
{CRON_PRESETS.map((p) => (
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-muted-foreground">{t("vmLxc.scheduled.cronExpression")}</Label>
<Input
value={scheduleCron}
onChange={(e) => { setScheduleCron(e.target.value); setSchedulePreset("custom") }}
placeholder={t("vmLxc.scheduled.cronPlaceholder")}
className="h-8 text-sm mt-1 font-mono"
/>
</div>
</div>
<div>
<Label className="text-xs text-muted-foreground">{t("vmLxc.scheduled.releaseDelayLabel")}</Label>
<Select value={String(scheduleReleaseDelayDays)} onValueChange={(v) => setScheduleReleaseDelayDays(Number(v))}>
<SelectTrigger className="h-8 text-sm mt-1 max-w-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="0">{t("vmLxc.scheduled.releaseDelayNone")}</SelectItem>
{[1, 3, 7, 14].map((days) => (
<SelectItem key={days} value={String(days)}>{t("vmLxc.scheduled.releaseDelayDays", { days })}</SelectItem>
))}
</SelectContent>
</Select>
<div className="text-[10px] text-muted-foreground mt-1 max-w-xl">{t("vmLxc.ociUpdates.releaseDelayHelp")}</div>
</div>
{ociInstance.host_directories && (
<div className="flex items-start gap-2 text-sm">
<Checkbox
id="oci-ack-external"
checked={scheduleAckExternal}
onCheckedChange={(v) => setScheduleAckExternal(Boolean(v))}
className="mt-0.5"
/>
<Label htmlFor="oci-ack-external" className="leading-tight cursor-pointer">
<span>{t("vmLxc.ociUpdates.ackExternal")}</span>
<div className="text-xs text-muted-foreground mt-0.5">{t("vmLxc.ociUpdates.ackExternalHelp")}</div>
</Label>
</div>
)}
{renderScheduleRunDetails()}
</div>
)}
{optionsEditMode && scheduleConfigured && (
<div className="flex justify-end">
<button
type="button"
onClick={deleteScheduleFromOptions}
disabled={scheduleSaving}
className="h-8 px-3 text-xs rounded-md border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 text-red-400 transition-colors inline-flex items-center gap-1.5 disabled:opacity-60"
>
<Trash2 className="h-3.5 w-3.5" />
{t("vmLxc.scheduled.deleteButton")}
</button>
</div>
)}
{scheduleError && <div className="text-xs text-red-400">{scheduleError}</div>}
</div>
<div className="pt-4 border-t border-border/50 flex items-center justify-end gap-2">
{!optionsEditMode ? (
<button
type="button"
onClick={enterOptionsEdit}
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5"
>
<Settings2 className="h-3.5 w-3.5" />
{t("vmLxc.options.editButton")}
</button>
) : (
<>
<button
type="button"
onClick={cancelOptionsEdit}
disabled={scheduleSaving}
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5 disabled:opacity-60"
>
{t("vmLxc.options.cancelButton")}
</button>
<button
type="button"
onClick={saveOptionsEdit}
disabled={scheduleSaving || (scheduleEnabled && ociInstance.host_directories && !scheduleAckExternal)}
className="h-8 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-40 inline-flex items-center gap-1.5"
>
{scheduleSaving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />}
{t("vmLxc.options.saveButton")}
</button>
</>
)}
</div>
</CardContent>
</Card>
</>
)
})()}
{/* Branch 1 — OCI-image container not installed by
OCI manager Apps */}
{!selectedVM.update_check?.managed_oci_app && !ociInstance &&
selectedVM.update_check?.is_oci_lxc && (
<Card className="border border-border bg-card/50">
<CardContent className="p-4 space-y-2">
@@ -5164,7 +5521,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
Individual actions stay with their section. The
optional reusable bulk action is configured in its
own card immediately before Options. */}
{!selectedVM.update_check?.managed_oci_app &&
{!selectedVM.update_check?.managed_oci_app && !ociInstance &&
!selectedVM.update_check?.is_oci_lxc && (() => {
const uc = selectedVM.update_check
const osUpdateStatusKnown = !!uc && !uc.error
@@ -6957,6 +7314,14 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
</Card>
</div>
)}
{activeModalTab === "logs" && selectedVM?.type === "lxc" && (
<OciConsoleLogPanel
key={selectedVM.vmid}
vmid={selectedVM.vmid}
running={selectedVM.status === "running"}
/>
)}
</div>
<div className="border-t border-border bg-background px-6 py-4 mt-auto shrink-0">
@@ -7390,6 +7755,37 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
}}
/>
)}
{/* Update or recreate an OCI instance: the same flow as OCI manager
Apps -> Manage installed OCI applications, for this container. */}
{ociAction && (
<ScriptTerminalModal
open={!!ociAction}
onClose={() => {
const vmid = ociAction.vmid
setOciAction(null)
// The image, the digest and the record changed: read them again.
fetchApi(`/api/vms/${vmid}/apps/check`, { method: "POST" })
.catch(() => undefined)
.finally(() => {
invalidateLxcApps(vmid)
vmModalCacheRef.current.mountPoints.delete(vmid)
fetchOciInstance(vmid)
fetchMountPoints(vmid)
mutate()
})
}}
scriptPath="/usr/local/share/proxmenux/scripts/oci/manage_instance.sh"
scriptName="oci_manage_instance"
title={ociAction.action === "update" ? t("vmLxc.ociUpdates.terminalTitleUpdate") : t("vmLxc.ociUpdates.terminalTitleRecreate")}
description={t("vmLxc.ociUpdates.terminalDescription")}
params={{
VMID: String(ociAction.vmid),
ACTION: ociAction.action,
KEEP_BACKUP: ociAction.action === "update" && applyBackup ? (applyBackupStorage || selectedBackupStorage || "") : "",
}}
/>
)}
</div>
)
}