"use client"

/**
 * LxcAppPanel — Body of the "App" tab in the LXC modal.
 *
 * Handles a LIST of apps per CT (one CT can host several services
 * — e.g. Frigate on port 5000 + go2rtc on 1984, or Docker + two
 * containerised apps).  Each app has:
 *   • an install method (dpkg / apk / file / binary / docker)
 *   • an optional GitHub repo for upstream version tracking
 *   • a list of ports, each with a description and web path
 *
 * Docker apps are "register-only": they exist to produce clickable
 * links, ProxMenux does NOT try to track their version and NEVER
 * emits warnings for them — updates for Docker apps are handled by
 * Docker itself.
 *
 * For ProxMenux-managed OCI CTs (Secure Gateway) the panel is
 * read-only — the actual update lifecycle lives in Security →
 * Secure Gateway.
 */

import { useCallback, useEffect, useMemo, useState } from "react"
import {
  Loader2, Save, RefreshCw, Trash2, Package, ExternalLink,
  AlertTriangle, Info, PlusCircle, Pencil, ChevronDown, ChevronRight, EyeOff,
  ArrowUpCircle, RotateCcw, Check, Settings2, ShieldCheck, CheckCircle2,
} from "lucide-react"
import { Card, CardContent } from "./ui/card"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Badge } from "./ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config"
import { useT } from "@/lib/i18n/provider"

// installed_via is optional now — an empty value means "register only,
// no version tracking, no warnings, just a clickable link". Docker
// apps and casual "just want a link" registrations use this default.
type InstalledVia = "" | "dpkg" | "apk" | "file" | "binary" |
                    "python_dist" | "docker_label" | "docker_exec" |
                    "command" | "manual"
type GithubSource = "releases" | "tags"

interface PortEntry {
  port: number | ""
  description?: string
  scheme?: "http" | "https"
  web_path?: string
  logo_url?: string
}

interface AppConfig {
  name: string
  installed_via?: InstalledVia
  package?: string
  file_path?: string
  file_regex?: string
  binary_path?: string
  binary_args?: string[]
  python_path?: string
  distribution?: string
  container_name?: string
  label?: string
  command_argv?: string[]
  installed_version?: string
  installed_regex?: string
  // Upstream source discriminator + fields. When `upstream_type` is
  // "github" (default when `repo` is set) the classic repo /
  // github_source / tag_regex fields drive the check. "http_json" and
  // "docker_hub" open two new source types validated separately on
  // the backend.
  upstream_type?: "github" | "http_json" | "docker_hub" | ""
  repo?: string
  github_source?: GithubSource
  upstream_url?: string
  upstream_json_path?: string
  docker_image?: string
  tag_regex?: string
  ports: PortEntry[]
  health_path?: string
  logo_url?: string
  helper_slug?: string
}

interface DetectedApp {
  slug: string
  name: string
  logo_url?: string | null
  default_ports?: number[]
  tracking_suggestion?: TrackingSuggestion | null
}

interface AppState {
  installed_version: string | null
  latest_version: string | null
  update_available: boolean | null
  error: string | null
  checked_at: string | null
}

interface AppEntry extends AppConfig {
  id: string
  state?: AppState
  created_at?: string
}

interface SidecarResponse {
  vmid: number
  apps: AppEntry[]
  dismissed_slugs?: string[]
  created_at?: string
  updated_at?: string
}

interface TrackingSuggestion {
  installed_via: "dpkg" | "apk" | "file" | "binary"
  package?: string
  file_path?: string
  file_regex?: string
  binary_path?: string
  repo?: string
  github_source?: "releases" | "tags"
  tag_regex?: string
}

interface Suggestions {
  name_suggestion: string | null
  helper_slug: string | null
  port_suggestions: number[]
  web_path_hint: string | null
  tracking_suggestion?: TrackingSuggestion | null
  default_ports?: number[]
  logo_url?: string | null
  extras?: DetectedApp[]
}

// Compact catalog entry — one row for every registerable app the
// picker can offer. Fetched once from /api/apps/catalog on panel
// mount, filtered client-side while the user types.
interface CatalogEntry {
  slug: string
  name: string
  logo: string
  default_port: number
  has_tracking: boolean
}

// Full detail for a picked catalog entry — server merges catalog
// metadata + curated tracking_suggestion (when available) so the
// editor can pre-fill every field in one round-trip.
interface CatalogDetail {
  slug: string
  name: string
  logo_url: string | null
  website: string
  default_ports: number[]
  tracking_suggestion?: TrackingSuggestion | null
}

interface ManagedAppInfo {
  managed_oci_app_id: string
  name: string
  installed_version?: string | null
  latest_version?: string | null
  update_available?: boolean | null
  checked_at?: string | null
  error?: string | null
}

interface Props {
  vmid: number
  ctIp?: string | null
  onChange?: () => void
  managed?: ManagedAppInfo | null
}

const EMPTY_APP: AppConfig = {
  name: "",
  installed_via: "",       // no tracking by default — just a link
  package: "",
  upstream_type: "",
  repo: "",
  github_source: "releases",
  upstream_url: "",
  upstream_json_path: "",
  docker_image: "",
  tag_regex: "v?(\\d+\\.\\d+\\.\\d+)",
  ports: [],
  logo_url: "",
}

// Default scheme heuristic for freshly-added ports — only used to
// pre-select the dropdown. The user always has the final say via
// the http/https selector next to the port input.
const HTTPS_HINT_PORTS = new Set([443, 4443, 8443, 9443])
const defaultSchemeFor = (port: number | ""): "http" | "https" =>
  HTTPS_HINT_PORTS.has(Number(port)) ? "https" : "http"

function buildWebUrl(ip: string | undefined | null, port: number | "", scheme?: "http" | "https") {
  if (!ip || ip === "DHCP" || !port) return null
  return `${scheme || defaultSchemeFor(port)}://${ip}:${port}`
}

// Suggest a dpkg/apk package name from a friendly app name — lowercase,
// spaces and slashes to hyphens, drop punctuation. Only used as a
// placeholder / auto-fill; user can always override.
function suggestPackageName(name: string) {
  return name
    .trim()
    .toLowerCase()
    .replace(/[\s/]+/g, "-")
    .replace(/[^a-z0-9._+@:-]/g, "")
    .replace(/-{2,}/g, "-")
    .replace(/^-+|-+$/g, "")
}

export function LxcAppPanel({ vmid, ctIp, onChange, managed }: Props) {
  const t = useT()
  const [loading, setLoading] = useState(true)
  const [sidecar, setSidecar] = useState<SidecarResponse | null>(null)
  const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
  const [error, setError] = useState<string | null>(null)
  // Editor state
  const [editing, setEditing] = useState<{ appId: string | null; draft: AppConfig } | null>(null)
  const [saving, setSaving] = useState(false)
  const [busyAppId, setBusyAppId] = useState<string | null>(null)
  // Advanced section (version tracking) is collapsed by default so the
  // basic Name + Ports flow stays approachable. Auto-expanded when
  // editing an app that already has installed_via set, or when the
  // user clicked Register on an auto-detected chip whose hint carries
  // tracking metadata — the user sees the fields we auto-filled and
  // can tweak or opt out before saving.
  const [showAdvanced, setShowAdvanced] = useState(false)

  // Catalog picker: 700+ apps fetched once from /api/apps/catalog and
  // filtered client-side while the user types in the Name input. The
  // dropdown shows top 20 matches. Selecting one calls the detail
  // endpoter to seed name / logo / ports / tracking_suggestion at once.
  const [catalog, setCatalog] = useState<CatalogEntry[]>([])
  const [pickerOpen, setPickerOpen] = useState(false)

  // "Register a different app" browse panel: when the user has hidden
  // some detections we surface them here with a Restore button before
  // falling through to the blank-form path. If there's nothing to
  // restore, this panel is skipped entirely and the button opens the
  // editor directly (fast path for the common case).
  const [browseOpen, setBrowseOpen] = useState(false)

  // Global "manage apps" mode. When ON, every app card grows a footer
  // with Remove / Check / Edit fields actions. When OFF the cards are
  // pure info — the only surfaced action is a hover-reveal Check icon
  // on the LATEST UPSTREAM panel. Toggled from a single button next to
  // "Add another application".
  const [editMode, setEditMode] = useState(false)

  const load = useCallback(async () => {
    if (managed) { setLoading(false); return }
    setLoading(true)
    setError(null)
    try {
      const r: SidecarResponse = await fetchApi(`/api/vms/${vmid}/apps`)
      setSidecar(r)
      // Always fetch suggestions — used both by the empty-state form
      // seed AND by the "Also detected on this container" chip strip
      // that surfaces unregistered detections even after the CT already
      // has ≥1 registered app. Previously gated on `!r.apps?.length`,
      // which meant a CT with 1 registered + 1 detected-but-not-yet-
      // registered app never showed the second app until the user
      // opened + cancelled the editor (which triggered a re-load path
      // that happened to fetch it).
      try {
        const s: Suggestions = await fetchApi(`/api/vms/${vmid}/apps/suggestions`)
        setSuggestions(s)
      } catch { /* non-fatal */ }
    } catch (e: any) {
      setError(e?.message || t("vmLxc.appEditor.loadFailed"))
    } finally {
      setLoading(false)
    }
  }, [vmid, managed])

  useEffect(() => { load() }, [load])

  // Fetch the picker catalog once per panel mount. Best-effort — if
  // the API is unreachable, the picker just stays empty and users
  // type the app name manually (same as before this feature).
  useEffect(() => {
    let cancelled = false
    fetchApi("/api/apps/catalog")
      .then((data: CatalogEntry[]) => {
        if (!cancelled && Array.isArray(data)) setCatalog(data)
      })
      .catch(() => { /* non-fatal */ })
    return () => { cancelled = true }
  }, [])

  // Derived state — computed here BEFORE any conditional early
  // return so React sees the same hook order on every render.
  // Rules of Hooks: `useMemo` after an `if (loading) return …`
  // trips React error #310 the moment `loading` flips false.
  const apps = sidecar?.apps || []

  // Unified detection list — primary community-scripts install +
  // every other app whose install signature was found on the CT
  // (`extras[]` from the backend). Both use the same DetectedApp
  // shape so the empty state renders them uniformly.
  const detectedList: DetectedApp[] = useMemo(() => {
    if (!suggestions) return []
    const out: DetectedApp[] = []
    if (suggestions.helper_slug && suggestions.name_suggestion) {
      out.push({
        slug: suggestions.helper_slug,
        name: suggestions.name_suggestion,
        logo_url: suggestions.logo_url,
        default_ports: suggestions.default_ports,
        tracking_suggestion: suggestions.tracking_suggestion,
      })
    }
    const seen = new Set(out.map((d) => d.slug))
    for (const e of suggestions.extras || []) {
      if (!seen.has(e.slug)) {
        out.push(e)
        seen.add(e.slug)
      }
    }
    return out
  }, [suggestions])

  // Registered slugs — used to filter the detection list down to
  // what the user hasn't already registered on this CT.
  const registeredSlugs = useMemo(
    () => new Set(apps.map((a) => a.helper_slug).filter(Boolean) as string[]),
    [apps],
  )
  // Dismissed slugs — persisted in the sidecar. Chips the user
  // explicitly hid via the ✕ button stay hidden across reloads until
  // they register the app (which also un-dismisses implicitly).
  const dismissedSlugs = useMemo(
    () => new Set(sidecar?.dismissed_slugs || []),
    [sidecar],
  )
  const visibleDetected = detectedList.filter(
    (d) => !registeredSlugs.has(d.slug) && !dismissedSlugs.has(d.slug),
  )
  // Alias for pre-existing consumers (post-registration chip strip).
  const unregisteredDetected = visibleDetected
  // Detections the user hid and could restore from the Register-a-
  // different-app panel. Not affected by registration state.
  const hiddenDetections = detectedList.filter((d) => dismissedSlugs.has(d.slug))

  // "Register a different app" behavior: if there are hidden slugs,
  // surface them first (with Restore) so the user can bring one back
  // instead of typing everything by hand. If nothing to restore, go
  // straight to the blank editor.
  const openBrowseOrEditor = () => {
    if (hiddenDetections.length > 0) setBrowseOpen(true)
    else openEditor()
  }

  const openEditor = useCallback(async (
    existing?: AppEntry,
    opts?: { withTracking?: boolean, preset?: DetectedApp },
  ) => {
    let seed: AppConfig
    if (existing) {
      seed = {
        name: existing.name,
        installed_via: (existing.installed_via as InstalledVia) || "",
        package: existing.package || "",
        file_path: existing.file_path || "",
        file_regex: existing.file_regex || "",
        binary_path: existing.binary_path || "",
        binary_args: existing.binary_args ? [...existing.binary_args] : [],
        python_path: existing.python_path || "",
        distribution: existing.distribution || "",
        container_name: existing.container_name || "",
        label: existing.label || "",
        command_argv: existing.command_argv ? [...existing.command_argv] : [],
        installed_version: existing.installed_version || "",
        installed_regex: existing.installed_regex || "",
        upstream_type: existing.upstream_type || (existing.repo ? "github" : ""),
        repo: existing.repo || "",
        github_source: existing.github_source || "releases",
        upstream_url: existing.upstream_url || "",
        upstream_json_path: existing.upstream_json_path || "",
        docker_image: existing.docker_image || "",
        tag_regex: existing.tag_regex || "v?(\\d+\\.\\d+\\.\\d+)",
        ports: existing.ports?.length ? existing.ports.map((p) => ({ ...p })) : [],
        health_path: existing.health_path || "",
        logo_url: existing.logo_url || "",
        helper_slug: existing.helper_slug || "",
      }
      // Editing an existing app: expand Advanced when tracking is on
      setShowAdvanced(!!seed.installed_via)
    } else {
      seed = { ...EMPTY_APP, ports: [] }
      let s = suggestions
      if (!s) {
        try {
          s = await fetchApi(`/api/vms/${vmid}/apps/suggestions`)
          setSuggestions(s)
        } catch { /* non-fatal */ }
      }
      // Preset path: a chip in the empty state (primary OR extra) was
      // clicked. Seed EVERYTHING from the preset so this works
      // regardless of whether it's the first or Nth app on the CT.
      // Primary detection is `{...suggestions}`-shaped, an extra is
      // `DetectedApp`-shaped — both carry name/logo/ports/tracking.
      if (opts?.preset) {
        const p = opts.preset
        seed.name = p.name
        seed.logo_url = p.logo_url || ""
        seed.helper_slug = p.slug
        if (p.default_ports?.length) {
          seed.ports = p.default_ports.map((port) => ({
            port,
            scheme: defaultSchemeFor(port),
            web_path: s?.web_path_hint || "",
          }))
        }
        if (opts.withTracking && p.tracking_suggestion) {
          const t = p.tracking_suggestion
          seed = {
            ...seed,
            installed_via: t.installed_via,
            package: t.package || "",
            file_path: t.file_path || "",
            file_regex: t.file_regex || "",
            binary_path: t.binary_path || "",
            upstream_type: (t as any).upstream_type || (t.repo ? "github" : ""),
            repo: t.repo || "",
            github_source: t.github_source || "releases",
            upstream_url: (t as any).upstream_url || "",
            upstream_json_path: (t as any).upstream_json_path || "",
            docker_image: (t as any).docker_image || "",
            tag_regex: t.tag_regex || "v?(\\d+\\.\\d+\\.\\d+)",
          }
          setShowAdvanced(true)
        } else {
          setShowAdvanced(false)
        }
      } else {
        // No preset (bare "+ Register application"): start empty so
        // the user types name/ports/logo for a custom app the auto-
        // detector doesn't know about.
        setShowAdvanced(false)
      }
    }
    setEditing({ appId: existing?.id || null, draft: seed })
    setError(null)
  }, [suggestions, vmid, sidecar])

  const closeEditor = () => {
    setEditing(null)
    setError(null)
  }

  const save = async () => {
    if (!editing) return
    setSaving(true)
    setError(null)
    try {
      const url = editing.appId
        ? `/api/vms/${vmid}/apps/${editing.appId}`
        : `/api/vms/${vmid}/apps`
      const method = editing.appId ? "PUT" : "POST"
      const r: SidecarResponse & { error?: string } = await fetchApi(url, {
        method,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(editing.draft),
      })
      if ((r as any).error) throw new Error((r as any).error)
      setSidecar(r)
      setEditing(null)
      onChange?.()
    } catch (e: any) {
      setError(e?.message || t("vmLxc.appEditor.saveFailed"))
    } finally {
      setSaving(false)
    }
  }

  const checkOne = async (appId: string) => {
    setBusyAppId(appId)
    setError(null)
    try {
      const r: SidecarResponse = await fetchApi(`/api/vms/${vmid}/apps/${appId}/check`, {
        method: "POST",
      })
      setSidecar(r)
      onChange?.()
    } catch (e: any) {
      setError(e?.message || t("vmLxc.appEditor.checkFailed"))
    } finally {
      setBusyAppId(null)
    }
  }

  const removeOne = async (appId: string) => {
    if (!confirm("Remove this application from the CT's App tab?")) return
    setBusyAppId(appId)
    setError(null)
    try {
      await fetchApi(`/api/vms/${vmid}/apps/${appId}`, { method: "DELETE" })
      // Reload from server so the empty state re-fetches suggestions
      await load()
      onChange?.()
    } catch (e: any) {
      setError(e?.message || t("vmLxc.appEditor.deleteFailed"))
    } finally {
      setBusyAppId(null)
    }
  }

  // Hide an auto-detected chip. Optimistic UI: update the local
  // sidecar state immediately so the chip disappears without
  // waiting for the round-trip, then persist to the server. If the
  // POST fails, reload from server to resync.
  const dismissDetection = async (slug: string, name: string) => {
    if (!confirm(t("vmLxc.appEditor.confirmHide", { name }))) return
    setSidecar((prev) => prev
      ? { ...prev, dismissed_slugs: [...(prev.dismissed_slugs || []), slug] }
      : prev,
    )
    try {
      const r: SidecarResponse = await fetchApi(`/api/vms/${vmid}/apps/dismiss`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ slug, dismissed: true }),
      })
      setSidecar(r)
    } catch (e: any) {
      setError(e?.message || t("vmLxc.appEditor.dismissFailed"))
      await load()  // resync on failure
    }
  }

  // Un-hide a previously dismissed slug. Used from the "Register a
  // different app" panel. Optimistically drops the slug from local
  // dismissed_slugs so the chip re-appears in the main list, then
  // persists.
  const restoreDetection = async (slug: string) => {
    setSidecar((prev) => prev
      ? { ...prev, dismissed_slugs: (prev.dismissed_slugs || []).filter((s) => s !== slug) }
      : prev,
    )
    try {
      const r: SidecarResponse = await fetchApi(`/api/vms/${vmid}/apps/dismiss`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ slug, dismissed: false }),
      })
      setSidecar(r)
    } catch (e: any) {
      setError(e?.message || t("vmLxc.appEditor.restoreFailed"))
      await load()
    }
  }

  // ── Managed CT (Secure Gateway etc.) ──────────────────────────
  // Mirrors the visual identity of a regular app card so managed OCI
  // apps sit next to user-registered apps without a jarring style
  // shift. Version data comes from managed_installs.update_check
  // (already tracked by oci_manager — same source as the Security →
  // Secure Gateway page). No footer: no Edit, no Check, no Remove —
  // the whole lifecycle lives in Security → Secure Gateway.
  if (managed) {
    // Currently the only OCI managed app is Secure Gateway (Tailscale
    // in an Alpine CT). When we add more OCI apps we'll swap this to
    // a lookup keyed on managed_oci_app_id → catalog metadata.
    const isSecureGateway = managed.managed_oci_app_id === "secure-gateway"
    const displayName = isSecureGateway ? "Secure Gateway" : (managed.name || t("vmLxc.appEditor.managedApp"))
    const displaySubtitle = isSecureGateway ? "Tailscale VPN Gateway" : ""
    // Two variants — the selfh.st mark (dark logo on light bg) reads
    // better in light mode; the homarr-labs "-light" variant (light
    // logo on dark bg) reads better in dark mode. Both are rendered
    // and Tailwind's dark: class picks which one is visible.
    const upstreamLogoLightUrl = isSecureGateway
      ? "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tailscale.webp"
      : ""
    const upstreamLogoDarkUrl = isSecureGateway
      ? "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/webp/tailscale-light.webp"
      : ""
    const upstreamName = isSecureGateway ? "Tailscale" : ""
    const repo = isSecureGateway ? "tailscale/tailscale" : ""
    const methodLine = isSecureGateway ? `apk · tailscale · ${t("vmLxc.appEditor.managedStatus")}` : t("vmLxc.appEditor.managedStatus")
    const hasUpdate = managed.update_available === true
    const upToDate = managed.update_available === false && !!managed.installed_version
    const showVersions = !!(managed.installed_version || managed.latest_version || repo)

    return (
      <div className="space-y-4">
        <Card className="border border-border bg-card/50">
          <CardContent className="p-4">
            {/* Block 1 — Secure Gateway identity (the ProxMenux
                product). Big shield, title, catalog subtitle. */}
            <div className="flex items-start gap-3">
              <div className="h-14 w-14 flex-shrink-0 rounded-md bg-cyan-500/10 flex items-center justify-center">
                <ShieldCheck className="h-11 w-11 text-cyan-500" />
              </div>
              <div className="min-w-0 flex-1">
                <h3 className="text-xl font-semibold text-foreground truncate">{displayName}</h3>
                {displaySubtitle && (
                  <div className="text-sm text-muted-foreground mt-0.5 truncate">{displaySubtitle}</div>
                )}
              </div>
            </div>

            {/* Block 2 — Underlying engine (Tailscale). Same visual
                pattern as a regular app card so it's clear this is
                what version tracking is anchored to. Repo link goes
                here (top-right on desktop / stacked on mobile) because
                the repo is the engine's, not Secure Gateway's. */}
            {upstreamName && (
              <div className="mt-4 pt-4 border-t border-border/50">
                <div className="flex items-start justify-between gap-3">
                  <div className="flex items-center gap-3 min-w-0 flex-1">
                    {upstreamLogoLightUrl || upstreamLogoDarkUrl ? (
                      <>
                        {upstreamLogoLightUrl && (
                          <img
                            src={upstreamLogoLightUrl}
                            alt=""
                            className="h-10 w-10 flex-shrink-0 rounded-md object-contain block dark:hidden"
                            onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
                          />
                        )}
                        {upstreamLogoDarkUrl && (
                          <img
                            src={upstreamLogoDarkUrl}
                            alt=""
                            className="h-10 w-10 flex-shrink-0 rounded-md object-contain hidden dark:block"
                            onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
                          />
                        )}
                      </>
                    ) : (
                      <div className="h-10 w-10 flex-shrink-0 rounded-md bg-muted flex items-center justify-center">
                        <Package className="h-5 w-5 text-muted-foreground" />
                      </div>
                    )}
                    <div className="min-w-0 flex-1">
                      <div className="text-base font-semibold text-foreground truncate">{upstreamName}</div>
                      <div className="text-xs text-muted-foreground mt-0.5">{methodLine}</div>
                      {managed.checked_at && (
                        <div className="text-[10px] text-muted-foreground/80 mt-0.5">
                          {t("vmLxc.appEditor.checkedAt", { date: new Date(managed.checked_at).toLocaleString([], { dateStyle: "short", timeStyle: "short" }) })}
                        </div>
                      )}
                      {repo && (
                        <a
                          href={`https://github.com/${repo}`}
                          target="_blank"
                          rel="noopener noreferrer"
                          className="md:hidden mt-1 text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1 min-w-0"
                        >
                          <ExternalLink className="h-3 w-3 flex-shrink-0" />
                          <span className="truncate">{repo}</span>
                        </a>
                      )}
                    </div>
                  </div>
                  {repo && (
                    <a
                      href={`https://github.com/${repo}`}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="hidden md:inline-flex text-xs text-muted-foreground hover:text-foreground items-center gap-1 flex-shrink-0 mt-1"
                    >
                      {repo}
                      <ExternalLink className="h-3 w-3" />
                    </a>
                  )}
                </div>
              </div>
            )}

            {showVersions && (
              <div className={"mt-3 grid gap-3 " + (repo ? "grid-cols-2" : "grid-cols-1")}>
                {managed.installed_version && (
                  <div className="p-3 rounded-md bg-muted/40">
                    <div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">{t("vmLxc.appEditor.installedStatus")}</div>
                    <div className="text-lg font-semibold font-mono text-foreground flex items-center gap-2">
                      {managed.installed_version}
                      {upToDate && (
                        <CheckCircle2 className="h-5 w-5 text-green-500 flex-shrink-0" aria-label={t("vmLxc.appEditor.upToDateBadge")} />
                      )}
                    </div>
                  </div>
                )}
                {repo && (
                  <div className="p-3 rounded-md bg-muted/40">
                    <div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">
                      {t("vmLxc.appEditor.latestUpstream")}
                    </div>
                    <div className={"text-lg font-semibold font-mono flex items-center gap-2 " + (hasUpdate ? "text-purple-400" : "text-foreground")}>
                      {managed.latest_version || <span className="text-muted-foreground text-base font-normal">{t("vmLxc.appEditor.checkingStatus")}</span>}
                      {hasUpdate && managed.latest_version && (
                        <ArrowUpCircle className="h-5 w-5 text-purple-400 flex-shrink-0" aria-label={t("vmLxc.appEditor.updateAvailableBadge")} />
                      )}
                    </div>
                  </div>
                )}
              </div>
            )}

            {managed.error && (
              <div className="mt-3 p-2 rounded-md bg-amber-500/10 border border-amber-500/30 flex items-start gap-2">
                <Info className="h-4 w-4 text-amber-400 flex-shrink-0 mt-0.5" />
                <div className="text-xs text-amber-300">{managed.error}</div>
              </div>
            )}

            {/* Managed banner — green translucent badge signalling this
                CT's lifecycle is owned by ProxMenux (not user CRUD). */}
            <div className="mt-4 p-2.5 rounded-md bg-green-500/10 border border-green-500/30 flex items-center gap-2 text-sm">
              <ShieldCheck className="h-4 w-4 text-green-400 flex-shrink-0" />
              <span className="text-green-300">{t("vmLxc.appEditor.installedManaged")}</span>
            </div>
          </CardContent>
        </Card>
      </div>
    )
  }

  if (loading) {
    return (
      <div className="flex items-center justify-center py-12 text-muted-foreground">
        <Loader2 className="h-5 w-5 animate-spin mr-2" />
        {t("vmLxc.appEditor.loadingApplications")}
      </div>
    )
  }

  // ── Editor ─────────────────────────────────────────────────────
  if (editing) {
    const draft = editing.draft
    const method = draft.installed_via || ""
    const isPackaged = method === "dpkg" || method === "apk"
    const setField = (patch: Partial<AppConfig>) =>
      setEditing({ ...editing, draft: { ...draft, ...patch } })
    // Editing the Name auto-fills the Package field on packaged
    // methods when it's still empty. Rationale: 90% of the time the
    // dpkg/apk package name mirrors the friendly app name (jellyfin,
    // adguardhome, portainer-ce). The user can still override.
    const setName = (name: string) => {
      const patch: Partial<AppConfig> = { name }
      if (isPackaged && !draft.package?.trim()) {
        patch.package = suggestPackageName(name)
      }
      setField(patch)
    }
    const setPort = (i: number, patch: Partial<PortEntry>) => {
      const ports = draft.ports.map((p, idx) => (idx === i ? { ...p, ...patch } : p))
      setField({ ports })
    }
    // "Add port" adds an EMPTY row for manual entry. Detected chips
    // ("+5000", "+1984"…) add the port directly with the port
    // pre-filled — no need for the user to open a row first.
    const addEmptyPort = () =>
      setField({ ports: [...draft.ports, { port: "", description: "", scheme: "http" }] })
    const addDetectedPort = (port: number) => {
      const scheme = defaultSchemeFor(port)
      // If the last row is still empty, fill it instead of appending
      // a duplicate. Avoids the two-lines-appear bug.
      const last = draft.ports[draft.ports.length - 1]
      if (last && last.port === "" && !last.description) {
        const ports = [...draft.ports]
        ports[ports.length - 1] = { port, description: "", scheme }
        setField({ ports })
      } else {
        setField({ ports: [...draft.ports, { port, description: "", scheme }] })
      }
    }
    const removePort = (i: number) =>
      setField({ ports: draft.ports.filter((_, idx) => idx !== i) })
    const usedPorts = new Set(draft.ports.map((p) => p.port))
    const suggestable = (suggestions?.port_suggestions || []).filter((p) => !usedPorts.has(p))

    return (
      <div className="space-y-4">
        {/* Editor card — opaque `bg-card` (not the `/50` used by
            view-mode cards) so the darker `bg-background` inputs sit
            visibly recessed against it. Matches the sunken-input
            pattern used in the PBS setup wizard: card = surface,
            fields = wells inside the surface. Reverts to `bg-card/50`
            automatically because this render branch only fires when
            `editing !== null`. */}
        <Card className="border border-border bg-card">
          <CardContent className="p-4 space-y-4">
            <div className="relative">
              <Label htmlFor="app-name">{t("vmLxc.appEditor.nameLabel")}</Label>
              <Input
                id="app-name"
                value={draft.name}
                onChange={(e) => { setName(e.target.value); setPickerOpen(true) }}
                onFocus={() => setPickerOpen(true)}
                onBlur={() => setTimeout(() => setPickerOpen(false), 150)}
                placeholder={suggestions?.name_suggestion || t("vmLxc.appEditor.nameSearchPlaceholder")}
                maxLength={64}
                autoComplete="off"
              />
              {/* Catalog picker dropdown — filters the 700+ helpers_cache
                  entries by name substring while the user types. Top 20
                  matches shown. Click one to auto-fill name / logo /
                  ports / tracking hint in one shot. Empty query with
                  the input focused shows a "start typing" hint. */}
              {pickerOpen && catalog.length > 0 && (() => {
                const q = (draft.name || "").trim().toLowerCase()
                if (!q) return null
                const matches = catalog
                  .filter((c) => c.name.toLowerCase().includes(q) || c.slug.toLowerCase().includes(q))
                  .slice(0, 20)
                if (!matches.length) return null
                return (
                  <div className="absolute z-20 left-0 right-0 mt-1 max-h-72 overflow-y-auto rounded-md border border-border bg-popover shadow-lg">
                    <div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted-foreground border-b border-border/60">
                      {matches.length === 20 ? t("vmLxc.appEditor.top20Matches") : t("vmLxc.appEditor.matchCount", { count: matches.length })}
                    </div>
                    {matches.map((c) => (
                      <button
                        key={c.slug}
                        type="button"
                        onMouseDown={(e) => e.preventDefault()}
                        onClick={async () => {
                          setPickerOpen(false)
                          try {
                            const detail: CatalogDetail = await fetchApi(`/api/apps/catalog/${c.slug}`)
                            // Seed the entire form from the picker detail.
                            const patch: Partial<AppConfig> = {
                              name: detail.name,
                              helper_slug: detail.slug,
                              logo_url: detail.logo_url || "",
                              ports: detail.default_ports?.length
                                ? detail.default_ports.map((p) => ({
                                    port: p,
                                    scheme: defaultSchemeFor(p),
                                    web_path: "",
                                  }))
                                : draft.ports,
                            }
                            if (detail.tracking_suggestion) {
                              const t = detail.tracking_suggestion as any
                              patch.installed_via = t.installed_via
                              patch.package = t.package || ""
                              patch.file_path = t.file_path || ""
                              patch.file_regex = t.file_regex || ""
                              patch.binary_path = t.binary_path || ""
                              patch.binary_args = t.binary_args || []
                              patch.python_path = t.python_path || ""
                              patch.distribution = t.distribution || ""
                              patch.container_name = t.container_name || ""
                              patch.label = t.label || ""
                              patch.installed_regex = t.installed_regex || ""
                              patch.upstream_type = (t as any).upstream_type || (t.repo ? "github" : "")
                              patch.repo = t.repo || ""
                              patch.github_source = t.github_source || "releases"
                              patch.upstream_url = (t as any).upstream_url || ""
                              patch.upstream_json_path = (t as any).upstream_json_path || ""
                              patch.docker_image = (t as any).docker_image || ""
                              patch.tag_regex = t.tag_regex || "v?(\\d+\\.\\d+\\.\\d+)"
                              setShowAdvanced(true)
                            }
                            setEditing((prev) => prev ? { ...prev, draft: { ...prev.draft, ...patch } } : prev)
                          } catch { /* non-fatal */ }
                        }}
                        className="w-full flex items-center gap-3 px-3 py-2 text-left hover:bg-accent hover:text-accent-foreground transition-colors"
                      >
                        {c.logo && (
                          <img src={c.logo} alt="" className="h-6 w-6 flex-shrink-0 rounded object-contain"
                               onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }} />
                        )}
                        <div className="flex-1 min-w-0">
                          <div className="text-sm text-foreground truncate">{c.name}</div>
                          <div className="text-[10px] text-muted-foreground">
                            {c.slug}
                            {c.default_port ? ` · ${t("vmLxc.appEditor.catalogPort", { port: c.default_port })}` : ""}
                            {c.has_tracking ? ` · ${t("vmLxc.appEditor.trackingAvailable")}` : ""}
                          </div>
                        </div>
                      </button>
                    ))}
                  </div>
                )
              })()}
            </div>

            {/* App-level logo URL — optional. Auto-filled from the
                catalog for helper-scripts installs, blank otherwise.
                For a custom app the user can paste a URL (typically
                from https://selfh.st/icons); empty → no logo in the
                app card header. */}
            <div className="pt-2 border-t border-border/50">
              <Label htmlFor="app-logo" className="text-xs uppercase tracking-wider text-muted-foreground">
                {t("vmLxc.appEditor.logoUrlLabel")} <span className="normal-case tracking-normal text-[10px] opacity-70">{t("vmLxc.appEditor.optionalSuffix")}</span>
              </Label>
              <Input
                id="app-logo"
                type="url"
                value={draft.logo_url || ""}
                onChange={(e) => setField({ logo_url: e.target.value })}
                placeholder={t("vmLxc.appEditor.portLogoPlaceholder")}
                maxLength={512}
                className="text-sm mt-2 font-mono"
              />
            </div>

            {/* Web links — each port becomes a clickable link (built as
                http[s]://<ip>:<port>). Detected chips add the port
                directly (no need to first open an empty row). */}
            <div className="pt-2 border-t border-border/50">
              <div className="flex items-center justify-between mb-2">
                <Label className="text-xs uppercase tracking-wider text-muted-foreground">
                  {t("vmLxc.appEditor.webLinks")}
                </Label>
                <Button type="button" variant="ghost" size="sm" onClick={addEmptyPort}>
                  <PlusCircle className="h-3.5 w-3.5 mr-1" />
                  {t("vmLxc.appEditor.addPort")}
                </Button>
              </div>

              {/* Detected chips FIRST — one-click add. Only shown when
                  there are chips left to suggest, so empty states stay
                  clean. Click on a chip: fills the current empty row
                  or adds a new one; never duplicates. */}
              {suggestable.length > 0 && (
                <div className="mb-2 flex flex-wrap gap-1 items-center">
                  <span className="text-[10px] text-muted-foreground pr-1">
                    {t("vmLxc.appEditor.detectedPorts")}
                  </span>
                  {suggestable.map((p) => (
                    <button
                      key={p}
                      type="button"
                      onClick={() => addDetectedPort(p)}
                      className="text-[10px] px-1.5 py-0.5 rounded bg-muted hover:bg-muted-foreground/20 text-foreground font-mono"
                    >
                      +{p}
                    </button>
                  ))}
                </div>
              )}

              {draft.ports.length === 0 && suggestable.length === 0 && (
                <div className="text-xs text-muted-foreground italic">
                  {t("vmLxc.appEditor.noWebPorts")}
                </div>
              )}

              <div className="space-y-3">
                {draft.ports.map((entry, i) => (
                  <div
                    key={i}
                    className="grid grid-cols-[86px_86px_1fr_auto] gap-2 items-center gap-y-1.5"
                  >
                    <Select
                      value={entry.scheme || defaultSchemeFor(entry.port)}
                      onValueChange={(v) => setPort(i, { scheme: v as "http" | "https" })}
                    >
                      <SelectTrigger className="text-sm h-9"><SelectValue /></SelectTrigger>
                      <SelectContent>
                        <SelectItem value="http">http</SelectItem>
                        <SelectItem value="https">https</SelectItem>
                      </SelectContent>
                    </Select>
                    <Input
                      type="number"
                      value={entry.port}
                      onChange={(e) => setPort(i, { port: e.target.value ? Number(e.target.value) : "" })}
                      placeholder={t("vmLxc.appEditor.portPortPlaceholder")}
                      min={1}
                      max={65535}
                      className="text-sm"
                    />
                    <Input
                      value={entry.description || ""}
                      onChange={(e) => setPort(i, { description: e.target.value })}
                      placeholder={t("vmLxc.appEditor.portDescriptionPlaceholder")}
                      maxLength={64}
                      className="text-sm"
                    />
                    <Button
                      type="button"
                      variant="ghost"
                      size="sm"
                      onClick={() => removePort(i)}
                      className="text-red-400 hover:text-red-300"
                      aria-label={t("vmLxc.appEditor.removePortTooltip")}
                    >
                      <Trash2 className="h-3.5 w-3.5" />
                    </Button>
                    {/* Per-link logo URL — spans cols 1-3 so its
                        right edge lines up with the description input
                        above (never covers the trash column). */}
                    <Input
                      value={entry.logo_url || ""}
                      onChange={(e) => setPort(i, { logo_url: e.target.value })}
                      placeholder={t("vmLxc.appEditor.portLogoLabel")}
                      maxLength={512}
                      className="col-start-1 col-end-4 text-xs font-mono h-8 opacity-70 focus:opacity-100"
                      type="url"
                    />
                  </div>
                ))}
              </div>
            </div>

            {/* ── Advanced (Version tracking, optional) ──────────
                Collapsed by default so casual users never see the
                technical fields. Auto-expanded when editing an app
                that already has tracking configured, or when the
                user registered from an auto-detected chip whose
                hint carried tracking metadata. */}
            <div className="pt-2 border-t border-border/50">
              <button
                type="button"
                onClick={() => setShowAdvanced(!showAdvanced)}
                className="flex items-center gap-1.5 text-xs uppercase tracking-wider text-muted-foreground hover:text-foreground w-full"
              >
                {showAdvanced ? (
                  <ChevronDown className="h-3.5 w-3.5" />
                ) : (
                  <ChevronRight className="h-3.5 w-3.5" />
                )}
                {t("vmLxc.appEditor.trackUpstream")}
                {!showAdvanced && !method && (
                  <span className="ml-auto text-[10px] normal-case tracking-normal">
                    {t("vmLxc.appEditor.trackOff")}
                  </span>
                )}
                {!showAdvanced && method && (
                  <span className="ml-auto text-[10px] normal-case tracking-normal text-emerald-400/80">
                    {t("vmLxc.appEditor.trackOn", { method })}
                  </span>
                )}
              </button>

              {showAdvanced && (
                <div className="mt-3 space-y-3">
                  <p className="text-xs text-muted-foreground leading-relaxed">
                    {t("vmLxc.appEditor.trackHelp")}
                  </p>

                  <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                    <div>
                      <Label htmlFor="app-method">{t("vmLxc.appEditor.installedViaLabel")}</Label>
                      <Select
                        value={method || "none"}
                        onValueChange={(v) =>
                          setField({ installed_via: v === "none" ? "" : (v as InstalledVia) })
                        }
                      >
                        <SelectTrigger id="app-method"><SelectValue /></SelectTrigger>
                        <SelectContent>
                          <SelectItem value="none">{t("vmLxc.appEditor.methodNone")}</SelectItem>
                          <SelectItem value="dpkg">{t("vmLxc.appEditor.methodDpkg")}</SelectItem>
                          <SelectItem value="apk">{t("vmLxc.appEditor.methodApk")}</SelectItem>
                          <SelectItem value="binary">{t("vmLxc.appEditor.binaryVersionHint")}</SelectItem>
                          <SelectItem value="file">{t("vmLxc.appEditor.methodFile")}</SelectItem>
                          <SelectItem value="python_dist">{t("vmLxc.appEditor.methodPython")}</SelectItem>
                          <SelectItem value="docker_label">{t("vmLxc.appEditor.methodDockerLabel")}</SelectItem>
                          <SelectItem value="docker_exec">{t("vmLxc.appEditor.methodDockerExec")}</SelectItem>
                          <SelectItem value="command">{t("vmLxc.appEditor.methodCommand")}</SelectItem>
                          <SelectItem value="manual">{t("vmLxc.appEditor.methodManual")}</SelectItem>
                        </SelectContent>
                      </Select>
                    </div>

                    {isPackaged && (
                      <div>
                        <Label htmlFor="app-package">{t("vmLxc.appEditor.packageIdentifierLabel")}</Label>
                        <Input
                          id="app-package"
                          value={draft.package || ""}
                          onChange={(e) => setField({ package: e.target.value })}
                          placeholder={method === "dpkg" ? "e.g. jellyfin-server" : "e.g. tailscale"}
                        />
                        <div className="text-[10px] text-muted-foreground mt-1">
                          Auto-filled from Name. Verify with{" "}
                          <code className="text-foreground/70">
                            {method === "dpkg" ? "dpkg -l | grep <app>" : "apk info | grep <app>"}
                          </code>{" "}
                          inside the CT.
                        </div>
                      </div>
                    )}

                    {method === "binary" && (
                      <div>
                        <Label htmlFor="app-binary">{t("vmLxc.appEditor.binaryPathLabel")}</Label>
                        <Input
                          id="app-binary"
                          value={draft.binary_path || ""}
                          onChange={(e) => setField({ binary_path: e.target.value })}
                          placeholder={t("vmLxc.appEditor.binaryPathPlaceholder")}
                          className="font-mono text-xs"
                        />
                        <div className="text-[10px] text-muted-foreground mt-1">
                          Absolute path. Find it with{" "}
                          <code className="text-foreground/70">which &lt;app&gt;</code> or{" "}
                          <code className="text-foreground/70">systemctl show &lt;service&gt; -p ExecStart</code>.
                        </div>
                      </div>
                    )}
                  </div>

                  {method === "file" && (
                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                      <div>
                        <Label htmlFor="app-file-path">{t("vmLxc.appEditor.filePathLabel")}</Label>
                        <Input
                          id="app-file-path"
                          value={draft.file_path || ""}
                          onChange={(e) => setField({ file_path: e.target.value })}
                          placeholder="/opt/app/VERSION"
                          className="font-mono text-xs"
                        />
                      </div>
                      <div>
                        <Label htmlFor="app-file-regex">{t("vmLxc.appEditor.regexCaptureGroupLabel")}</Label>
                        <Input
                          id="app-file-regex"
                          value={draft.file_regex || ""}
                          onChange={(e) => setField({ file_regex: e.target.value })}
                          placeholder={t("vmLxc.appEditor.regexPlaceholderVersion")}
                          className="font-mono text-xs"
                        />
                      </div>
                    </div>
                  )}

                  {method === "python_dist" && (
                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                      <div>
                        <Label htmlFor="app-py-path">{t("vmLxc.appEditor.pythonInterpreterLabel")}</Label>
                        <Input
                          id="app-py-path"
                          value={draft.python_path || ""}
                          onChange={(e) => setField({ python_path: e.target.value })}
                          placeholder={t("vmLxc.appEditor.pythonInterpreterPlaceholder")}
                          className="font-mono text-xs"
                        />
                      </div>
                      <div>
                        <Label htmlFor="app-py-dist">{t("vmLxc.appEditor.pipDistLabel")}</Label>
                        <Input
                          id="app-py-dist"
                          value={draft.distribution || ""}
                          onChange={(e) => setField({ distribution: e.target.value })}
                          placeholder={t("vmLxc.appEditor.pipDistPlaceholder")}
                          className="font-mono text-xs"
                        />
                      </div>
                    </div>
                  )}

                  {method === "docker_label" && (
                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                      <div>
                        <Label htmlFor="app-dl-container">{t("vmLxc.appEditor.containerNameLabel")}</Label>
                        <Input
                          id="app-dl-container"
                          value={draft.container_name || ""}
                          onChange={(e) => setField({ container_name: e.target.value })}
                          placeholder={t("vmLxc.appEditor.containerNamePlaceholder")}
                          className="font-mono text-xs"
                        />
                      </div>
                      <div>
                        <Label htmlFor="app-dl-label">{t("vmLxc.appEditor.ociLabelKeyLabel")}</Label>
                        <Input
                          id="app-dl-label"
                          value={draft.label || ""}
                          onChange={(e) => setField({ label: e.target.value })}
                          placeholder={t("vmLxc.appEditor.ociLabelPlaceholder")}
                          className="font-mono text-xs"
                        />
                      </div>
                    </div>
                  )}

                  {method === "docker_exec" && (
                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                      <div>
                        <Label htmlFor="app-de-container">{t("vmLxc.appEditor.containerNameLabel")}</Label>
                        <Input
                          id="app-de-container"
                          value={draft.container_name || ""}
                          onChange={(e) => setField({ container_name: e.target.value })}
                          placeholder={t("vmLxc.appEditor.containerNamePlaceholder")}
                          className="font-mono text-xs"
                        />
                      </div>
                      <div>
                        <Label htmlFor="app-de-binary">{t("vmLxc.appEditor.binaryPathBare")}</Label>
                        <Input
                          id="app-de-binary"
                          value={draft.binary_path || ""}
                          onChange={(e) => setField({ binary_path: e.target.value })}
                          placeholder={t("vmLxc.appEditor.binaryPathBarePlaceholder")}
                          className="font-mono text-xs"
                        />
                      </div>
                      <div className="sm:col-span-2">
                        <Label htmlFor="app-de-args">{t("vmLxc.appEditor.binaryArgsLabel")}</Label>
                        <Input
                          id="app-de-args"
                          value={(draft.binary_args || []).join(", ")}
                          onChange={(e) => setField({
                            binary_args: e.target.value.split(",").map(s => s.trim()).filter(Boolean),
                          })}
                          placeholder={t("vmLxc.appEditor.binaryArgsPlaceholder")}
                          className="font-mono text-xs"
                        />
                        <div className="text-[10px] text-muted-foreground mt-1">
                          {t("vmLxc.appEditor.binaryArgsHintPrefix")} <code className="text-foreground/70">--version</code>. {t("vmLxc.appEditor.binaryArgsHintGrafana")} <code className="text-foreground/70">server, -v</code>.
                        </div>
                      </div>
                    </div>
                  )}

                  {method === "command" && (
                    <div>
                      <Label htmlFor="app-cmd-argv">{t("vmLxc.appEditor.commandLabel")}</Label>
                      <Input
                        id="app-cmd-argv"
                        value={(draft.command_argv || []).join(", ")}
                        onChange={(e) => setField({
                          command_argv: e.target.value.split(",").map(s => s.trim()).filter(Boolean),
                        })}
                        placeholder={t("vmLxc.appEditor.commandPlaceholder")}
                        className="font-mono text-xs"
                      />
                      <div className="text-[10px] text-muted-foreground mt-1">
                        {t("vmLxc.appEditor.commandSafetyHelp")}
                      </div>
                      <div className="mt-2">
                        <Label htmlFor="app-cmd-regex">{t("vmLxc.appEditor.installedVersionRegexLabel")}</Label>
                        <Input
                          id="app-cmd-regex"
                          value={draft.installed_regex || ""}
                          onChange={(e) => setField({ installed_regex: e.target.value })}
                          placeholder={t("vmLxc.appEditor.tagRegexBare")}
                          className="font-mono text-xs mt-1"
                        />
                      </div>
                    </div>
                  )}

                  {method === "manual" && (
                    <div>
                      <Label htmlFor="app-manual-ver">{t("vmLxc.appEditor.installedVersionLabel")}</Label>
                      <Input
                        id="app-manual-ver"
                        value={draft.installed_version || ""}
                        onChange={(e) => setField({ installed_version: e.target.value })}
                        placeholder="1.2.3"
                        maxLength={64}
                        className="font-mono text-xs"
                      />
                      <div className="text-[10px] text-muted-foreground mt-1">
                        {t("vmLxc.appEditor.manualVersionHelp")}
                      </div>
                    </div>
                  )}

                  {method && (() => {
                    // Upstream source selector — 3 methods (github,
                    // http_json, docker_hub). Legacy sidecars with a
                    // `repo` set but no `upstream_type` default to
                    // github so the classic behaviour keeps working
                    // until the user re-saves.
                    const upstreamType = draft.upstream_type
                      || (draft.repo ? "github" : "")
                    const setUpstream = (t: "" | "github" | "http_json" | "docker_hub") => {
                      // Clear other-type fields when switching so the
                      // backend doesn't receive stale data.
                      const patch: Partial<AppConfig> = { upstream_type: t }
                      if (t !== "github") {
                        patch.repo = ""
                        patch.github_source = "releases"
                      }
                      if (t !== "http_json") {
                        patch.upstream_url = ""
                        patch.upstream_json_path = ""
                      }
                      if (t !== "docker_hub") {
                        patch.docker_image = ""
                      }
                      if (!t) patch.tag_regex = ""
                      setField(patch)
                    }
                    return (
                      <>
                        <div>
                          <Label htmlFor="app-upstream-type">{t("vmLxc.appEditor.upstreamSourceLabel")}</Label>
                          <Select
                            value={upstreamType || "none"}
                            onValueChange={(v) => setUpstream(v === "none" ? "" : (v as any))}
                          >
                            <SelectTrigger id="app-upstream-type"><SelectValue /></SelectTrigger>
                            <SelectContent>
                              <SelectItem value="none">{t("vmLxc.appEditor.upstreamNone")}</SelectItem>
                              <SelectItem value="github">{t("vmLxc.appEditor.upstreamGithub")}</SelectItem>
                              <SelectItem value="http_json">{t("vmLxc.appEditor.upstreamHttp")}</SelectItem>
                              <SelectItem value="docker_hub">{t("vmLxc.appEditor.upstreamDocker")}</SelectItem>
                            </SelectContent>
                          </Select>
                          <div className="text-[10px] text-muted-foreground mt-1">
                            {t("vmLxc.appEditor.upstreamHelp")}
                          </div>
                        </div>

                        {upstreamType === "github" && (
                          <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                            <div>
                              <Label htmlFor="app-repo">{t("vmLxc.appEditor.githubRepoLabel")}</Label>
                              <Input
                                id="app-repo"
                                value={draft.repo || ""}
                                onChange={(e) => setField({ repo: e.target.value })}
                                placeholder={t("vmLxc.appEditor.githubRepoPlaceholder")}
                              />
                              <div className="text-[10px] text-muted-foreground mt-1">
                                {t("vmLxc.appEditor.githubRepoHelp")}
                              </div>
                            </div>
                            <div>
                              <Label htmlFor="app-source">{t("vmLxc.appEditor.latestFromLabel")}</Label>
                              <Select
                                value={draft.github_source || "releases"}
                                onValueChange={(v) => setField({ github_source: v as GithubSource })}
                              >
                                <SelectTrigger id="app-source"><SelectValue /></SelectTrigger>
                                <SelectContent>
                                  <SelectItem value="releases">{t("vmLxc.appEditor.sourceReleases")}</SelectItem>
                                  <SelectItem value="tags">{t("vmLxc.appEditor.sourceTags")}</SelectItem>
                                </SelectContent>
                              </Select>
                            </div>
                          </div>
                        )}

                        {upstreamType === "http_json" && (
                          <div className="space-y-3">
                            <div>
                              <Label htmlFor="app-upstream-url">{t("vmLxc.appEditor.endpointUrlLabel")}</Label>
                              <Input
                                id="app-upstream-url"
                                type="url"
                                value={draft.upstream_url || ""}
                                onChange={(e) => setField({ upstream_url: e.target.value })}
                                placeholder={t("vmLxc.appEditor.endpointUrlPlaceholder")}
                                className="font-mono text-xs"
                                maxLength={512}
                              />
                              <div className="text-[10px] text-muted-foreground mt-1">
                                {t("vmLxc.appEditor.httpJsonHelp")}
                              </div>
                            </div>
                            <div>
                              <Label htmlFor="app-upstream-path">{t("vmLxc.appEditor.jsonPathLabel")}</Label>
                              <Input
                                id="app-upstream-path"
                                value={draft.upstream_json_path || ""}
                                onChange={(e) => setField({ upstream_json_path: e.target.value })}
                                placeholder={t("vmLxc.appEditor.jsonPathPlaceholder")}
                                className="font-mono text-xs"
                                maxLength={128}
                              />
                              <div className="text-[10px] text-muted-foreground mt-1">
                                {t("vmLxc.appEditor.jsonPathHelp")}
                              </div>
                            </div>
                          </div>
                        )}

                        {upstreamType === "docker_hub" && (
                          <div>
                            <Label htmlFor="app-docker-image">{t("vmLxc.appEditor.dockerImageLabel")}</Label>
                            <Input
                              id="app-docker-image"
                              value={draft.docker_image || ""}
                              onChange={(e) => setField({ docker_image: e.target.value })}
                              placeholder={t("vmLxc.appEditor.dockerImagePlaceholder")}
                              className="font-mono text-xs"
                              maxLength={255}
                            />
                            <div className="text-[10px] text-muted-foreground mt-1">
                              <code className="text-foreground/70">owner/name</code> — or bare
                              name for official images. ProxMenux picks the highest semver tag
                              matching the filter below.
                            </div>
                          </div>
                        )}

                        {upstreamType && (
                          <div>
                            <Label htmlFor="app-tag-regex">
                              {upstreamType === "docker_hub" ? t("vmLxc.appEditor.tagFilterRegexOptional") : t("vmLxc.appEditor.versionRegexOptional")}
                            </Label>
                            <Input
                              id="app-tag-regex"
                              value={draft.tag_regex || ""}
                              onChange={(e) => setField({ tag_regex: e.target.value })}
                              placeholder={t("vmLxc.appEditor.tagRegexPlaceholder")}
                              className="font-mono text-xs"
                            />
                            <div className="text-[10px] text-muted-foreground mt-1">
                              {upstreamType === "github" && "Extracts the version from the release tag name."}
                              {upstreamType === "http_json" && "Optional — extract a substring from the endpoint's value."}
                              {upstreamType === "docker_hub" && "Filter which tags qualify (e.g. only semver). Applied before picking the highest."}
                            </div>
                          </div>
                        )}
                      </>
                    )
                  })()}
                </div>
              )}
            </div>

            {error && (
              <div className="text-xs text-red-400 flex items-start gap-1.5">
                <AlertTriangle className="h-3.5 w-3.5 flex-shrink-0 mt-0.5" />
                <span>{error}</span>
              </div>
            )}

            <div className="flex flex-wrap gap-2 justify-end pt-2">
              <Button variant="ghost" onClick={closeEditor} disabled={saving}>{t("vmLxc.appEditor.cancelButton")}</Button>
              <Button
                onClick={save}
                disabled={saving || !draft.name.trim()}
                className="bg-blue-500 hover:bg-blue-600 text-white"
              >
                {saving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <Save className="h-4 w-4 mr-1.5" />}
                {t("vmLxc.appEditor.saveButton")}
              </Button>
            </div>
          </CardContent>
        </Card>
      </div>
    )
  }

  // Restore chip — same visual shell as detection chip but the
  // action switches from Register/Hide to Restore. Used inside the
  // Register-a-different-app panel when hidden slugs exist.
  const renderRestoreChip = (d: DetectedApp) => (
    <div key={d.slug} className="p-3 rounded-md border border-border/60 bg-background/40">
      <div className="flex flex-col sm:flex-row sm:items-center gap-3">
        <div className="flex items-center gap-3 min-w-0 flex-1">
          {d.logo_url && (
            <img
              src={d.logo_url}
              alt=""
              className="h-14 w-14 flex-shrink-0 rounded-md object-contain"
              onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
            />
          )}
          <div className="min-w-0">
            <div className="text-sm font-semibold text-foreground truncate">{d.name}</div>
            <div className="text-xs text-muted-foreground">{t("vmLxc.appEditor.hiddenBadge")}</div>
          </div>
        </div>
        <Button
          size="sm"
          variant="outline"
          onClick={() => restoreDetection(d.slug)}
          className="w-full sm:w-auto"
        >
          <RotateCcw className="h-3.5 w-3.5 mr-1.5" />
          {t("vmLxc.appEditor.restoreButton")}
        </Button>
      </div>
    </div>
  )

  // Uniform detection chip used in every context — empty state,
  // post-registration "also detected" strip, and the Register-a-
  // different-app panel. Actions layout responsive:
  //   • Desktop (sm+): Register + Hide side-by-side, both labeled
  //   • Mobile: same row, 3/4 Register (label+icon) + 1/4 Hide
  //     (eye icon only inside a red-translucent button)
  //
  // Single "Register" button covers both paths — with or without
  // tracking hint — the editor opens pre-filled with whatever data
  // we have, and the user can adjust in Advanced.
  const renderDetectionChip = (d: DetectedApp) => (
    <div key={d.slug} className="p-3 rounded-md border border-border/60 bg-background/40">
      <div className="flex flex-col sm:flex-row sm:items-center gap-3">
        <div className="flex items-center gap-3 min-w-0 flex-1">
          {d.logo_url && (
            <img
              src={d.logo_url}
              alt=""
              className="h-14 w-14 flex-shrink-0 rounded-md object-contain"
              onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
            />
          )}
          <div className="min-w-0">
            <div className="text-sm font-semibold text-foreground truncate">{d.name}</div>
            <div className="text-xs text-emerald-400/90">{t("vmLxc.appEditor.detectedInContainer")}</div>
          </div>
        </div>
        <div className="flex flex-row gap-2 flex-shrink-0 sm:justify-end w-full sm:w-auto">
          <Button
            size="sm"
            onClick={() => openEditor(undefined, { withTracking: !!d.tracking_suggestion, preset: d })}
            className="flex-[3] sm:flex-none bg-blue-500 hover:bg-blue-600 text-white"
          >
            <PlusCircle className="h-3.5 w-3.5 mr-1" />
            {t("vmLxc.appEditor.registerButton")}
          </Button>
          <Button
            size="sm"
            variant="ghost"
            onClick={() => dismissDetection(d.slug, d.name)}
            aria-label={`Hide ${d.name} detection`}
            title={t("vmLxc.appEditor.hidePermanentlyTooltip")}
            className="flex-1 sm:flex-none bg-red-500/10 hover:bg-red-500/20 border border-red-500/30 text-red-400 hover:text-red-300"
          >
            <EyeOff className="h-3.5 w-3.5 sm:mr-1" />
            <span className="hidden sm:inline">{t("vmLxc.appEditor.hideButton")}</span>
          </Button>
        </div>
      </div>
    </div>
  )

  return (
    <div className="space-y-4">
      {/* Browse panel — surfaces hidden detections with Restore
          before falling through to the blank-form path. Rendered
          before app cards / empty state so it takes precedence when
          open. Closes automatically once all hidden slugs are
          restored (nothing left to show → back to normal flow). */}
      {browseOpen && (
        <Card className="border border-border bg-card/50">
          <CardContent className="p-4 space-y-3">
            <div className="flex items-center justify-between gap-2">
              <div>
                <h3 className="text-sm font-semibold text-foreground">{t("vmLxc.appEditor.registerDifferent")}</h3>
                <p className="text-xs text-muted-foreground mt-0.5">
                  {t(hiddenDetections.length === 1 ? "vmLxc.appEditor.hiddenDetectionsHelpSingular" : "vmLxc.appEditor.hiddenDetectionsHelpPlural", { count: hiddenDetections.length })}
                </p>
              </div>
              <Button
                variant="ghost"
                size="sm"
                onClick={() => setBrowseOpen(false)}
                aria-label={t("vmLxc.appEditor.closePanel")}
                className="text-muted-foreground hover:text-foreground"
              >
                {t("vmLxc.appEditor.cancelButton")}
              </Button>
            </div>
            {hiddenDetections.length > 0 && (
              <div className="space-y-2">
                {hiddenDetections.map(renderRestoreChip)}
              </div>
            )}
            <div className="pt-1 flex justify-center">
              <Button
                onClick={() => { setBrowseOpen(false); openEditor() }}
                className="bg-blue-500 hover:bg-blue-600 text-white"
              >
                <PlusCircle className="h-4 w-4 mr-1.5" />
                {t("vmLxc.appEditor.registerCustom")}
              </Button>
            </div>
          </CardContent>
        </Card>
      )}

      {/* Empty state — always uniform chip list regardless of how
          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 && (
        <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">
              <Package className="h-5 w-5 text-emerald-400" />
            </div>
            <h3 className="text-sm font-semibold text-foreground text-center">
              {t("vmLxc.appEditor.noAppsTitle")}
            </h3>
            <p className="text-xs text-muted-foreground max-w-md mx-auto leading-relaxed text-center">
              {t("vmLxc.appEditor.noAppsBody")}
            </p>
            {visibleDetected.length > 0 && (
              <div className="space-y-2 pt-2">
                {visibleDetected.map(renderDetectionChip)}
              </div>
            )}
            <div className="pt-1 flex justify-center">
              <Button onClick={openBrowseOrEditor} variant={visibleDetected.length > 0 ? "outline" : "default"}
                      className={visibleDetected.length > 0 ? "" : "bg-blue-500 hover:bg-blue-600 text-white"}>
                <PlusCircle className="h-4 w-4 mr-1.5" />
                {visibleDetected.length > 0 ? t("vmLxc.appEditor.registerDifferent") : t("vmLxc.appEditor.registerApplication")}
                {hiddenDetections.length > 0 && (
                  <span className="ml-2 text-[10px] opacity-70">
                    · {t("vmLxc.appEditor.hiddenSuffix", { count: hiddenDetections.length })}
                  </span>
                )}
              </Button>
            </div>
          </CardContent>
        </Card>
      )}

      {/* App cards */}
      {apps.map((app) => {
        const st = app.state
        // Version tracking is on when installed_via is set. Without a
        // method the app is register-only — no cards, no warnings.
        const tracking = !!app.installed_via
        const hasUpdate = tracking && st?.update_available === true
        const upToDate = tracking && st?.update_available === false && !!st?.installed_version
        return (
          <Card key={app.id} className="border border-border bg-card/50">
            <CardContent className="p-4">
              <div className="flex items-start justify-between gap-3 mb-3">
                <div className="flex items-center gap-3 min-w-0 flex-1">
                  {app.logo_url && (
                    <img
                      src={app.logo_url}
                      alt=""
                      className="h-14 w-14 flex-shrink-0 rounded-md object-contain"
                      onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
                    />
                  )}
                  <div className="min-w-0 flex-1">
                    <h3 className="text-lg font-semibold text-foreground truncate">{app.name}</h3>
                    {tracking && (
                      <div className="text-xs text-muted-foreground mt-0.5 flex items-center gap-1.5 flex-wrap">
                        {app.installed_via === "dpkg" && app.package && <>dpkg · <code className="text-foreground/80">{app.package}</code></>}
                        {app.installed_via === "apk" && app.package && <>apk · <code className="text-foreground/80">{app.package}</code></>}
                        {app.installed_via === "file" && app.file_path && <>file · <code className="text-foreground/80">{app.file_path}</code></>}
                        {app.installed_via === "binary" && app.binary_path && <>binary · <code className="text-foreground/80">{app.binary_path}</code></>}
                      </div>
                    )}
                    {tracking && st?.checked_at && (
                      <div className="text-[10px] text-muted-foreground/80 mt-0.5">
                        Checked {new Date(st.checked_at).toLocaleString([], { dateStyle: "short", timeStyle: "short" })}
                      </div>
                    )}
                    {/* Mobile-only repo link: falls into the metadata
                        stack below Checked, full-width so long repo
                        names wrap cleanly instead of competing with
                        the top-right on narrow screens. */}
                    {app.repo && tracking && (
                      <a
                        href={`https://github.com/${app.repo}`}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="md:hidden mt-1 text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1 min-w-0"
                      >
                        <ExternalLink className="h-3 w-3 flex-shrink-0" />
                        <span className="truncate">{app.repo}</span>
                      </a>
                    )}
                  </div>
                </div>
                {/* Desktop-only repo link: same row as the title on md+,
                    hidden on mobile where the stacked variant above
                    handles it. */}
                {app.repo && tracking && (
                  <a
                    href={`https://github.com/${app.repo}`}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="hidden md:inline-flex text-xs text-muted-foreground hover:text-foreground items-center gap-1 flex-shrink-0 mt-1"
                  >
                    {app.repo}
                    <ExternalLink className="h-3 w-3" />
                  </a>
                )}
              </div>

              {/* Version panels — always same-line (grid-cols-2), on
                  every viewport when ANY upstream source is configured
                  (github repo, http_json endpoint, or docker_hub
                  image). The signal that an update exists is the
                  LATEST UPSTREAM number turning purple + a small
                  ArrowUpCircle next to it, matching the project-wide
                  update button convention. Absence of purple = up to
                  date; no colored banners, no green/orange noise.
                  Labels stay muted so numbers keep visual priority.
                  When no upstream is configured, the LATEST panel is
                  omitted entirely and INSTALLED takes the full width. */}
              {(() => {
                const hasUpstream = !!(app.repo || app.upstream_type)
                if (!tracking || !(st?.installed_version || hasUpstream)) return null
                return (
                  <div className={"mb-3 grid gap-3 " + (hasUpstream ? "grid-cols-2" : "grid-cols-1")}>
                    {st?.installed_version && (
                      <div className="p-3 rounded-md bg-muted/40">
                        <div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">{t("vmLxc.appEditor.installedStatus")}</div>
                        <div className="text-lg font-semibold font-mono text-foreground flex items-center gap-2">
                          {st.installed_version}
                          {upToDate && (
                            <CheckCircle2 className="h-5 w-5 text-green-500 flex-shrink-0" aria-label={t("vmLxc.appEditor.upToDateBadge")} />
                          )}
                        </div>
                      </div>
                    )}
                    {hasUpstream && (
                      <div className="p-3 rounded-md bg-muted/40">
                        <div className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">
                          {t("vmLxc.appEditor.latestUpstream")}
                        </div>
                        <div className={"text-lg font-semibold font-mono flex items-center gap-2 " + (hasUpdate ? "text-purple-400" : "text-foreground")}>
                          {st?.latest_version || <span className="text-muted-foreground text-base font-normal">{t("vmLxc.appEditor.checkingStatus")}</span>}
                          {hasUpdate && st?.latest_version && (
                            <ArrowUpCircle className="h-5 w-5 text-purple-400 flex-shrink-0" aria-label={t("vmLxc.appEditor.updateAvailableBadge")} />
                          )}
                        </div>
                      </div>
                    )}
                  </div>
                )
              })()}

              {tracking && st?.error && (
                <div className="mb-3 p-2 rounded-md bg-amber-500/10 border border-amber-500/30 flex items-start gap-2">
                  <Info className="h-4 w-4 text-amber-400 flex-shrink-0 mt-0.5" />
                  <div className="text-xs text-amber-300">{st.error}</div>
                </div>
              )}

              {/* Web links — one row per port. Each row:
                    [logo 56px]  Description or app name
                                 ↗ http://IP:PORT
                  Logo is optional (per-port `logo_url`); when absent
                  the row indents naturally to align with the text.
                  If we can't resolve an IP for the CT we hide the row. */}
              {app.ports && app.ports.length > 0 && (
                <div className="mb-3 space-y-4">
                  {app.ports.map((p) => {
                    const url = buildWebUrl(ctIp, p.port, p.scheme)
                    if (!url) return null
                    const label = p.description || app.name
                    return (
                      <div key={p.port} className="flex items-start gap-3 min-w-0">
                        {p.logo_url && (
                          <img
                            src={p.logo_url}
                            alt=""
                            className="h-14 w-14 flex-shrink-0 rounded-md object-contain"
                            onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none" }}
                          />
                        )}
                        <div className="min-w-0 flex flex-col">
                          <span className="text-sm font-medium text-foreground truncate">{label}</span>
                          <a
                            href={url}
                            target="_blank"
                            rel="noopener noreferrer"
                            className="text-blue-400 hover:text-blue-300 inline-flex items-center gap-1.5 min-w-0"
                            title={url}
                          >
                            <ExternalLink className="h-3.5 w-3.5 flex-shrink-0" />
                            <span className="font-mono text-sm truncate">{url}</span>
                          </a>
                        </div>
                      </div>
                    )
                  })}
                </div>
              )}

              {/* Footer with per-card actions — only rendered in the
                  global edit mode (toggled from the "Edit" button next
                  to Add another application). View mode keeps cards
                  chrome-free; Check is still reachable via the
                  hover-reveal icon on the LATEST panel. Buttons match
                  the Settings-page section style (h-8, outline,
                  small icon + label) for visual consistency across
                  the app. */}
              {editMode && (
                <div className="flex flex-wrap gap-2 items-center mt-3 pt-3 border-t border-border/50">
                  <button
                    type="button"
                    onClick={() => removeOne(app.id)}
                    disabled={busyAppId === app.id}
                    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.appEditor.removeButton")}
                  </button>
                  <div className="ml-auto flex flex-wrap gap-2">
                    {tracking && (
                      <button
                        type="button"
                        onClick={() => checkOne(app.id)}
                        disabled={busyAppId === app.id}
                        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"
                      >
                        {busyAppId === app.id
                          ? <Loader2 className="h-3.5 w-3.5 animate-spin" />
                          : <RefreshCw className="h-3.5 w-3.5" />}
                        {t("vmLxc.appEditor.checkButton")}
                      </button>
                    )}
                    <button
                      type="button"
                      onClick={() => openEditor(app)}
                      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.appEditor.editFieldsButton")}
                    </button>
                  </div>
                </div>
              )}
            </CardContent>
          </Card>
        )
      })}

      {/* Post-registration "also detected" strip — every hint slug
          whose install signature is present on the CT AND that
          hasn't been registered yet is shown as a chip with a
          one-click Register button. Filtered against the sidecar's
          `helper_slug` field so a registered app never re-appears. */}
      {apps.length > 0 && unregisteredDetected.length > 0 && (
        <div className="space-y-2">
          <div className="text-xs uppercase tracking-wider text-muted-foreground">
            {t("vmLxc.appEditor.alsoDetectedContainer")}
          </div>
          {unregisteredDetected.map(renderDetectionChip)}
        </div>
      )}

      {/* Add-more + Edit toggle. Edit is a global toggle that reveals
          the per-card action footer (Remove / Check / Edit fields).
          Add-more is disabled while editing so the two flows don't
          overlap. Routes through the browse panel if there are hidden
          detections, so the user gets one-click Restore before hand-
          typing a custom app. */}
      {apps.length > 0 && (
        <div className="flex justify-end items-center gap-2">
          <Button
            variant="outline"
            size="sm"
            onClick={openBrowseOrEditor}
            disabled={editMode}
          >
            <PlusCircle className="h-4 w-4 mr-1.5" />
            {t("vmLxc.appEditor.addAnotherApplication")}
            {hiddenDetections.length > 0 && (
              <span className="ml-2 text-[10px] opacity-70">
                · {t("vmLxc.appEditor.hiddenSuffix", { count: hiddenDetections.length })}
              </span>
            )}
          </Button>
          <button
            type="button"
            onClick={() => setEditMode((v) => !v)}
            className="h-9 px-3 text-sm rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5"
          >
            {editMode ? (
              <>
                <Check className="h-3.5 w-3.5" />
                {t("vmLxc.appEditor.doneButton")}
              </>
            ) : (
              <>
                <Pencil className="h-3.5 w-3.5" />
                {t("vmLxc.appEditor.editButton")}
              </>
            )}
          </button>
        </div>
      )}

      {error && (
        <div className="text-xs text-red-400 flex items-start gap-1.5">
          <AlertTriangle className="h-3.5 w-3.5 flex-shrink-0 mt-0.5" />
          <span>{error}</span>
        </div>
      )}
    </div>
  )
}
