mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-09 06:56:37 +00:00
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:
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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 */ }
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user