feat(oci): GPU selection per host and per image, one notification per update, and App tab for stack containers

- Immich asks what runs its video and its recognition in one menu, in both
  modes, and gives the GPU to the server and to Machine learning; AMD uses ROCm
- Frigate, Ollama, llama.cpp, Faster Whisper and Piper take the image built
  for the chosen GPU
- The acceleration menu offers only what the host can run
- An update or a recreation sends one notification with its result instead of
  the stop, backup and start of each container
- A private bridge with nothing connected is not reported as down
- Secondary containers of a stack appear in the App tab with their version and
  logo; Secure Gateway shows the same update state in both views
- A mistyped value in the wizard asks the same question again
This commit is contained in:
MacRimi
2026-10-02 21:45:38 +02:00
parent 20ee21c08f
commit 9b5cefb81a
55 changed files with 2294 additions and 113 deletions
+37 -3
View File
@@ -343,6 +343,10 @@ interface Props {
ctIp?: string | null
onChange?: () => void
managed?: ManagedAppInfo | null
// What the OCI record says about this container, known without probing
// it: whether ProxMenux installed it from an image, and whether it is a
// secondary container of a multi-container application.
oci?: { instance: boolean; memberOf?: { vmid: number; label: string } | null } | null
// Optional seed payload from the parent's cross-open ref cache. When
// supplied, the panel renders with real content on the very first
// frame and only revalidates silently in the background — no
@@ -443,7 +447,7 @@ function parseArgvInput(value: string): string[] {
return value.split(",").map((item) => item.trim()).filter(Boolean)
}
export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Props) {
export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: Props) {
const t = useT()
const isLightTheme = useIsLightTheme()
// Seed from `initialData` first, then fall back to the shared cache
@@ -704,7 +708,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
// record names, so there is nothing to search for and nothing else to add.
// What can go stale is what the record and the registry say, and this
// reads both again.
const isOciInstall = !!suggestions?.oci_instance
const isOciInstall = !!suggestions?.oci_instance || !!oci?.instance
const ociMemberOf = oci?.memberOf ?? null
const isOciAdguard = suggestions?.oci_instance?.template_id === "image-adguard-home"
useEffect(() => {
if (!isOciAdguard) return
@@ -2582,7 +2587,36 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
many detections there are (0, 1, or many). Below the chips,
a single "Register a different app" button lets the user
add something the auto-detector doesn't know about. */}
{apps.length === 0 && (
{/* A database or a cache of a multi-container application has no
application of its own: it is reached from the main container. */}
{apps.length === 0 && ociMemberOf && (
<Card className="border border-border bg-card/50">
<CardContent className="p-6 space-y-3">
<div className="mx-auto p-2 rounded-full bg-blue-500/10 w-fit">
<Info className="h-5 w-5 text-blue-400" />
</div>
<h3 className="text-sm font-semibold text-foreground text-center">
{t("vmLxc.appEditor.stackMemberTitle")}
</h3>
<p className="text-xs text-muted-foreground max-w-md mx-auto leading-relaxed text-center">
{t("vmLxc.appEditor.stackMemberBody", { primary: ociMemberOf.label })}
</p>
<div className="pt-1 flex justify-center">
<Button
onClick={() => window.dispatchEvent(new CustomEvent("openLxcAppModal", {
detail: { vmid: ociMemberOf.vmid, tab: "app" },
}))}
className="bg-blue-500 hover:bg-blue-600 text-white"
>
<ChevronRight className="h-4 w-4 mr-1.5" />
{t("vmLxc.ociUpdates.openPrimary")}
</Button>
</div>
</CardContent>
</Card>
)}
{apps.length === 0 && !ociMemberOf && (
<Card className="border border-border bg-card/50">
<CardContent className="p-6 space-y-3">
<div className="mx-auto p-2 rounded-full bg-emerald-500/10 w-fit">
@@ -162,6 +162,19 @@ export function SecureGatewaySetup() {
loadInitialData()
}, [])
// The gateway was updated from the Updates tab of its container: read the
// update state and the status again so this card agrees with it.
useEffect(() => {
const refresh = (event: Event) => {
const detail = (event as CustomEvent).detail || {}
if (detail.appId !== "secure-gateway" || detail.source === "card") return
void loadUpdateInfo(true)
void loadStatus()
}
window.addEventListener("proxmenuxManagedAppUpdated", refresh)
return () => window.removeEventListener("proxmenuxManagedAppUpdated", refresh)
}, [])
const loadInitialData = async () => {
setLoading(true)
setLoadError(null)
@@ -288,6 +301,8 @@ export function SecureGatewaySetup() {
// Status may briefly show "stopped" if tailscale was restarted —
// refresh that too so the action buttons render the right state.
await loadStatus()
// The Updates tab of the gateway container reads it again.
window.dispatchEvent(new CustomEvent("proxmenuxManagedAppUpdated", { detail: { appId: "secure-gateway", source: "card" } }))
} else {
setUpdateError(res?.message || sg("errors.updateFailed"))
}
+24
View File
@@ -1102,6 +1102,14 @@ export function VirtualMachines() {
return () => window.removeEventListener("openLxcAppModal", handler as EventListener)
}, [vmData])
// An application ProxMenux manages (Secure Gateway) was updated or checked
// from its own card: read the guests again so its Updates tab agrees.
useEffect(() => {
const refresh = () => { mutate() }
window.addEventListener("proxmenuxManagedAppUpdated", refresh)
return () => window.removeEventListener("proxmenuxManagedAppUpdated", refresh)
}, [mutate])
// Same deep-link but for QEMU guests. VMs don't have the App tab,
// so we land on Status (which is what handleVMClick already
// defaults to — no override needed).
@@ -5057,6 +5065,20 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
ctIp={ctIp}
onChange={() => mutate()}
initialData={getLxcAppsCached(selectedVM.vmid) ?? null}
oci={ociInstance?.oci_instance ? {
instance: true,
memberOf: ociInstance.stack && ociInstance.primary_vmid !== selectedVM.vmid
? (() => {
const primaryVM = (vmData || []).find((v) => v.vmid === ociInstance.primary_vmid)
return {
vmid: ociInstance.primary_vmid,
label: primaryVM
? `${primaryVM.name} (CT ${ociInstance.primary_vmid})`
: `CT ${ociInstance.primary_vmid}`,
}
})()
: null,
} : null}
managed={
managedEntry
? {
@@ -5182,6 +5204,8 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
onClick={async () => {
try {
await fetchApi(`/api/oci/installed/${appId}/update`, { method: "POST" })
// The card of this application on the Security page reads it again.
window.dispatchEvent(new CustomEvent("proxmenuxManagedAppUpdated", { detail: { appId } }))
mutate()
} catch { /* opening the App tab surfaces the error */ }
}}