mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-29 18:16:43 +00:00
OCI catalog verification, console start marks and log cleanup
This commit is contained in:
@@ -24,12 +24,20 @@ interface ConsoleLogResponse {
|
|||||||
inode: number | null
|
inode: number | null
|
||||||
reset?: boolean
|
reset?: boolean
|
||||||
head_truncated?: boolean
|
head_truncated?: boolean
|
||||||
|
start_marks?: boolean
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const LINE_OPTIONS = [100, 500, 1000] as const
|
const LINE_OPTIONS = [100, 500, 1000] as const
|
||||||
const POLL_MS = 2000
|
const POLL_MS = 2000
|
||||||
|
|
||||||
|
// Written into the log by the container's pre-start hook at every start.
|
||||||
|
const START_MARK_RE = /^=== ProxMenux: container started (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) ===$/
|
||||||
|
const lastStart = (lines: string[]) => {
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) if (START_MARK_RE.test(lines[i])) return i
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
const lineTone = (line: string) => {
|
const lineTone = (line: string) => {
|
||||||
if (/\b(error|fatal|panic|critical|exception)\b/i.test(line)) return "text-red-400"
|
if (/\b(error|fatal|panic|critical|exception)\b/i.test(line)) return "text-red-400"
|
||||||
if (/\bwarn(ing)?\b/i.test(line)) return "text-amber-400"
|
if (/\bwarn(ing)?\b/i.test(line)) return "text-amber-400"
|
||||||
@@ -45,6 +53,7 @@ export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: b
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [follow, setFollow] = useState(true)
|
const [follow, setFollow] = useState(true)
|
||||||
const [filter, setFilter] = useState("")
|
const [filter, setFilter] = useState("")
|
||||||
|
const [sinceStart, setSinceStart] = useState(true)
|
||||||
const position = useRef<{ offset: number; inode: number | null } | null>(null)
|
const position = useRef<{ offset: number; inode: number | null } | null>(null)
|
||||||
const scroller = useRef<HTMLDivElement>(null)
|
const scroller = useRef<HTMLDivElement>(null)
|
||||||
const inFlight = useRef(false)
|
const inFlight = useRef(false)
|
||||||
@@ -53,7 +62,9 @@ export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: b
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const r = await fetchApi<ConsoleLogResponse>(`/api/lxc/${vmid}/console-log?lines=${lineCount}`)
|
const r = await fetchApi<ConsoleLogResponse>(
|
||||||
|
`/api/lxc/${vmid}/console-log?lines=${lineCount}${sinceStart ? "&since_start=1" : ""}`,
|
||||||
|
)
|
||||||
setEnabled(r.enabled)
|
setEnabled(r.enabled)
|
||||||
setLines(r.lines || [])
|
setLines(r.lines || [])
|
||||||
position.current = { offset: r.offset, inode: r.inode }
|
position.current = { offset: r.offset, inode: r.inode }
|
||||||
@@ -63,7 +74,7 @@ export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: b
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [vmid, lineCount])
|
}, [vmid, lineCount, sinceStart])
|
||||||
|
|
||||||
const poll = useCallback(async () => {
|
const poll = useCallback(async () => {
|
||||||
const pos = position.current
|
const pos = position.current
|
||||||
@@ -79,7 +90,10 @@ export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: b
|
|||||||
setLines(r.lines || [])
|
setLines(r.lines || [])
|
||||||
} else if (r.lines?.length) {
|
} else if (r.lines?.length) {
|
||||||
setLines((prev) => {
|
setLines((prev) => {
|
||||||
const next = prev.concat(r.lines)
|
let next = prev.concat(r.lines)
|
||||||
|
// A restart while following opens a new session.
|
||||||
|
const mark = sinceStart ? lastStart(next) : -1
|
||||||
|
if (mark > 0) next = next.slice(mark)
|
||||||
return next.length > lineCount ? next.slice(next.length - lineCount) : next
|
return next.length > lineCount ? next.slice(next.length - lineCount) : next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -88,7 +102,7 @@ export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: b
|
|||||||
} finally {
|
} finally {
|
||||||
inFlight.current = false
|
inFlight.current = false
|
||||||
}
|
}
|
||||||
}, [vmid, lineCount])
|
}, [vmid, lineCount, sinceStart])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
load()
|
||||||
@@ -105,7 +119,7 @@ export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: b
|
|||||||
|
|
||||||
const shown = useMemo(() => {
|
const shown = useMemo(() => {
|
||||||
const needle = filter.trim().toLowerCase()
|
const needle = filter.trim().toLowerCase()
|
||||||
return needle ? lines.filter((l) => l.toLowerCase().includes(needle)) : lines
|
return needle ? lines.filter((l) => START_MARK_RE.test(l) || l.toLowerCase().includes(needle)) : lines
|
||||||
}, [lines, filter])
|
}, [lines, filter])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -145,6 +159,15 @@ export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: b
|
|||||||
</div>
|
</div>
|
||||||
{enabled && (
|
{enabled && (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<Select value={sinceStart ? "start" : "all"} onValueChange={(v) => setSinceStart(v === "start")}>
|
||||||
|
<SelectTrigger className="h-7 w-auto text-xs gap-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="start" className="text-xs">{t("vmLxc.consoleLog.sinceStart")}</SelectItem>
|
||||||
|
<SelectItem value="all" className="text-xs">{t("vmLxc.consoleLog.allHistory")}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
<Select value={String(lineCount)} onValueChange={(v) => setLineCount(Number(v))}>
|
<Select value={String(lineCount)} onValueChange={(v) => setLineCount(Number(v))}>
|
||||||
<SelectTrigger className="h-7 w-auto text-xs gap-1">
|
<SelectTrigger className="h-7 w-auto text-xs gap-1">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -242,9 +265,22 @@ export function OciConsoleLogPanel({ vmid, running }: { vmid: number; running: b
|
|||||||
className="rounded-md border border-border bg-background/50 flex-1 overflow-y-auto min-h-0"
|
className="rounded-md border border-border bg-background/50 flex-1 overflow-y-auto min-h-0"
|
||||||
>
|
>
|
||||||
<pre className="text-[11px] font-mono leading-snug whitespace-pre-wrap break-all p-3">
|
<pre className="text-[11px] font-mono leading-snug whitespace-pre-wrap break-all p-3">
|
||||||
{shown.map((line, idx) => (
|
{shown.map((line, idx) => {
|
||||||
<div key={idx} className={lineTone(line)}>{line || " "}</div>
|
const mark = START_MARK_RE.exec(line)
|
||||||
))}
|
if (mark) {
|
||||||
|
const when = new Date(`${mark[1]}T${mark[2]}`)
|
||||||
|
return (
|
||||||
|
<div key={idx} className="my-1.5 flex items-center gap-2 text-sky-400/90 select-none">
|
||||||
|
<span className="h-px flex-1 bg-sky-500/30" />
|
||||||
|
<span>{t("vmLxc.consoleLog.startedAt", {
|
||||||
|
date: Number.isNaN(when.getTime()) ? `${mark[1]} ${mark[2]}` : when.toLocaleString(),
|
||||||
|
})}</span>
|
||||||
|
<span className="h-px flex-1 bg-sky-500/30" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <div key={idx} className={lineTone(line)}>{line || " "}</div>
|
||||||
|
})}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1178,6 +1178,9 @@
|
|||||||
"follow": "Folgen",
|
"follow": "Folgen",
|
||||||
"pause": "Pausieren",
|
"pause": "Pausieren",
|
||||||
"refresh": "Aktualisieren",
|
"refresh": "Aktualisieren",
|
||||||
|
"sinceStart": "Seit dem letzten Start",
|
||||||
|
"allHistory": "Gesamter Verlauf",
|
||||||
|
"startedAt": "Container gestartet · {date}",
|
||||||
"download": "Herunterladen",
|
"download": "Herunterladen",
|
||||||
"filter": "Zeilen filtern",
|
"filter": "Zeilen filtern",
|
||||||
"loading": "Konsolenausgabe wird geladen...",
|
"loading": "Konsolenausgabe wird geladen...",
|
||||||
|
|||||||
@@ -1199,6 +1199,9 @@
|
|||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
|
"sinceStart": "Since last start",
|
||||||
|
"allHistory": "Full history",
|
||||||
|
"startedAt": "Container started · {date}",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"filter": "Filter lines",
|
"filter": "Filter lines",
|
||||||
"loading": "Loading console output...",
|
"loading": "Loading console output...",
|
||||||
|
|||||||
@@ -1178,6 +1178,9 @@
|
|||||||
"follow": "Seguir",
|
"follow": "Seguir",
|
||||||
"pause": "Pausar",
|
"pause": "Pausar",
|
||||||
"refresh": "Actualizar",
|
"refresh": "Actualizar",
|
||||||
|
"sinceStart": "Desde el último arranque",
|
||||||
|
"allHistory": "Todo el historial",
|
||||||
|
"startedAt": "Contenedor iniciado · {date}",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
"filter": "Filtrar líneas",
|
"filter": "Filtrar líneas",
|
||||||
"loading": "Cargando la salida de consola...",
|
"loading": "Cargando la salida de consola...",
|
||||||
|
|||||||
@@ -1178,6 +1178,9 @@
|
|||||||
"follow": "Suivre",
|
"follow": "Suivre",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
"refresh": "Actualiser",
|
"refresh": "Actualiser",
|
||||||
|
"sinceStart": "Depuis le dernier démarrage",
|
||||||
|
"allHistory": "Historique complet",
|
||||||
|
"startedAt": "Conteneur démarré · {date}",
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
"filter": "Filtrer les lignes",
|
"filter": "Filtrer les lignes",
|
||||||
"loading": "Chargement de la sortie console...",
|
"loading": "Chargement de la sortie console...",
|
||||||
|
|||||||
@@ -1178,6 +1178,9 @@
|
|||||||
"follow": "Segui",
|
"follow": "Segui",
|
||||||
"pause": "Pausa",
|
"pause": "Pausa",
|
||||||
"refresh": "Aggiorna",
|
"refresh": "Aggiorna",
|
||||||
|
"sinceStart": "Dall'ultimo avvio",
|
||||||
|
"allHistory": "Cronologia completa",
|
||||||
|
"startedAt": "Container avviato · {date}",
|
||||||
"download": "Scarica",
|
"download": "Scarica",
|
||||||
"filter": "Filtra righe",
|
"filter": "Filtra righe",
|
||||||
"loading": "Caricamento dell'output della console...",
|
"loading": "Caricamento dell'output della console...",
|
||||||
|
|||||||
@@ -1178,6 +1178,9 @@
|
|||||||
"follow": "Seguir",
|
"follow": "Seguir",
|
||||||
"pause": "Pausar",
|
"pause": "Pausar",
|
||||||
"refresh": "Atualizar",
|
"refresh": "Atualizar",
|
||||||
|
"sinceStart": "Desde a última inicialização",
|
||||||
|
"allHistory": "Histórico completo",
|
||||||
|
"startedAt": "Contêiner iniciado · {date}",
|
||||||
"download": "Transferir",
|
"download": "Transferir",
|
||||||
"filter": "Filtrar linhas",
|
"filter": "Filtrar linhas",
|
||||||
"loading": "A carregar a saída da consola...",
|
"loading": "A carregar a saída da consola...",
|
||||||
|
|||||||
@@ -1199,6 +1199,9 @@
|
|||||||
"follow": "Sledovať",
|
"follow": "Sledovať",
|
||||||
"pause": "Pozastaviť",
|
"pause": "Pozastaviť",
|
||||||
"refresh": "Obnoviť",
|
"refresh": "Obnoviť",
|
||||||
|
"sinceStart": "Od posledného spustenia",
|
||||||
|
"allHistory": "Celá história",
|
||||||
|
"startedAt": "Kontajner spustený · {date}",
|
||||||
"download": "Stiahnuť",
|
"download": "Stiahnuť",
|
||||||
"filter": "Filtrovať riadky",
|
"filter": "Filtrovať riadky",
|
||||||
"loading": "Načítava sa výstup konzoly...",
|
"loading": "Načítava sa výstup konzoly...",
|
||||||
|
|||||||
@@ -1178,6 +1178,9 @@
|
|||||||
"follow": "Följ",
|
"follow": "Följ",
|
||||||
"pause": "Pausa",
|
"pause": "Pausa",
|
||||||
"refresh": "Uppdatera",
|
"refresh": "Uppdatera",
|
||||||
|
"sinceStart": "Sedan senaste start",
|
||||||
|
"allHistory": "Hela historiken",
|
||||||
|
"startedAt": "Containern startade · {date}",
|
||||||
"download": "Ladda ned",
|
"download": "Ladda ned",
|
||||||
"filter": "Filtrera rader",
|
"filter": "Filtrera rader",
|
||||||
"loading": "Läser in konsolutdata...",
|
"loading": "Läser in konsolutdata...",
|
||||||
|
|||||||
@@ -733,6 +733,36 @@ def _lxc_isolated_ips(vmid):
|
|||||||
return isolated
|
return isolated
|
||||||
|
|
||||||
|
|
||||||
|
def _lxc_shares_host_network(vmid):
|
||||||
|
"""Whether the container uses the host's network namespace instead of its
|
||||||
|
own, as a ProxMenux host-monitor (Glances, Netdata) does: lxc-info then
|
||||||
|
lists every address of the host, private bridges included."""
|
||||||
|
try:
|
||||||
|
with open(f"/etc/pve/lxc/{int(vmid)}.conf", encoding="utf-8") as handle:
|
||||||
|
text = handle.read().split("\n[", 1)[0]
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return False
|
||||||
|
return ("lxc.include: /etc/pve/lxc/proxmenux-host-monitor" in text
|
||||||
|
or ("lxc.net.0.type: none" in text and not re.search(r"^net\d+:", text, re.M)))
|
||||||
|
|
||||||
|
|
||||||
|
def _host_default_route_ipv4():
|
||||||
|
"""The host's IPv4 address on the interface that carries its default
|
||||||
|
route: the one a reader on the LAN reaches."""
|
||||||
|
try:
|
||||||
|
route = subprocess.run(['ip', '-4', 'route', 'show', 'default'],
|
||||||
|
capture_output=True, text=True, timeout=3).stdout
|
||||||
|
match = re.search(r"\bdev\s+(\S+)", route)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
addr = subprocess.run(['ip', '-4', '-o', 'addr', 'show', 'dev', match.group(1), 'scope', 'global'],
|
||||||
|
capture_output=True, text=True, timeout=3).stdout
|
||||||
|
found = re.search(r"\binet\s+([0-9.]+)/", addr)
|
||||||
|
return found.group(1) if found else None
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_lxc_ip_from_lxc_info(vmid):
|
def get_lxc_ip_from_lxc_info(vmid):
|
||||||
"""Get LXC IP addresses using lxc-info command (for DHCP containers)
|
"""Get LXC IP addresses using lxc-info command (for DHCP containers)
|
||||||
Returns a dict with all IPs and classification"""
|
Returns a dict with all IPs and classification"""
|
||||||
@@ -763,6 +793,10 @@ def get_lxc_ip_from_lxc_info(vmid):
|
|||||||
isolated = _lxc_isolated_ips(vmid)
|
isolated = _lxc_isolated_ips(vmid)
|
||||||
if isolated:
|
if isolated:
|
||||||
real_ips.sort(key=lambda ip: ip in isolated)
|
real_ips.sort(key=lambda ip: ip in isolated)
|
||||||
|
if _lxc_shares_host_network(vmid):
|
||||||
|
host_ip = _host_default_route_ipv4()
|
||||||
|
if host_ip in real_ips:
|
||||||
|
real_ips.sort(key=lambda ip: ip != host_ip)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'all_ips': ips,
|
'all_ips': ips,
|
||||||
@@ -15349,7 +15383,9 @@ def api_lxc_console_log(vmid):
|
|||||||
lines = request.args.get('lines', default=200, type=int)
|
lines = request.args.get('lines', default=200, type=int)
|
||||||
offset = request.args.get('offset', type=int)
|
offset = request.args.get('offset', type=int)
|
||||||
inode = request.args.get('inode', type=int)
|
inode = request.args.get('inode', type=int)
|
||||||
return jsonify(oci_console_logs.read(vmid, lines=lines, offset=offset, inode=inode))
|
since_start = request.args.get('since_start', '') in ('1', 'true')
|
||||||
|
return jsonify(oci_console_logs.read(vmid, lines=lines, offset=offset, inode=inode,
|
||||||
|
since_start=since_start))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"ok": False, "error": str(e)}), 500
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,11 @@ def clean(text: str) -> list[str]:
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def _read_tail(handle, size: int, lines: int) -> tuple[list[str], bool]:
|
# Written by the container's pre-start hook at every start (oci_console_mark.sh).
|
||||||
|
START_MARK = "=== ProxMenux: container started "
|
||||||
|
|
||||||
|
|
||||||
|
def _read_tail(handle, size: int, lines: int, since_start: bool = False) -> tuple[list[str], bool, bool]:
|
||||||
start = max(0, size - TAIL_READ_LIMIT)
|
start = max(0, size - TAIL_READ_LIMIT)
|
||||||
handle.seek(start)
|
handle.seek(start)
|
||||||
data = handle.read(size - start).decode("utf-8", errors="replace")
|
data = handle.read(size - start).decode("utf-8", errors="replace")
|
||||||
@@ -87,11 +91,15 @@ def _read_tail(handle, size: int, lines: int) -> tuple[list[str], bool]:
|
|||||||
result = result[1:] # the first line was cut by the read window
|
result = result[1:] # the first line was cut by the read window
|
||||||
if result and result[-1] == "":
|
if result and result[-1] == "":
|
||||||
result = result[:-1]
|
result = result[:-1]
|
||||||
head_cut = start > 0 or len(result) > lines
|
marks = [index for index, line in enumerate(result) if line.startswith(START_MARK)]
|
||||||
return result[-lines:], head_cut
|
if since_start and marks:
|
||||||
|
result = result[marks[-1]:]
|
||||||
|
head_cut = (start > 0 and not (since_start and marks)) or len(result) > lines
|
||||||
|
return result[-lines:], head_cut, bool(marks)
|
||||||
|
|
||||||
|
|
||||||
def read(vmid: int, lines: int = 200, offset: int | None = None, inode: int | None = None) -> dict:
|
def read(vmid: int, lines: int = 200, offset: int | None = None, inode: int | None = None,
|
||||||
|
since_start: bool = False) -> dict:
|
||||||
"""The last `lines` lines, or what was appended after `offset`.
|
"""The last `lines` lines, or what was appended after `offset`.
|
||||||
|
|
||||||
`lines=0` reads nothing: it only says whether the container has a
|
`lines=0` reads nothing: it only says whether the container has a
|
||||||
@@ -115,9 +123,9 @@ def read(vmid: int, lines: int = 200, offset: int | None = None, inode: int | No
|
|||||||
if offset is None or replaced or offset > size:
|
if offset is None or replaced or offset > size:
|
||||||
# First open, or the file the viewer followed is gone: rotated by
|
# First open, or the file the viewer followed is gone: rotated by
|
||||||
# copytruncate, or recreated with the container.
|
# copytruncate, or recreated with the container.
|
||||||
result, head_cut = _read_tail(handle, size, max(lines, 1))
|
result, head_cut, has_marks = _read_tail(handle, size, max(lines, 1), since_start)
|
||||||
return {**base, "lines": result, "offset": size, "reset": offset is not None,
|
return {**base, "lines": result, "offset": size, "reset": offset is not None,
|
||||||
"head_truncated": head_cut}
|
"head_truncated": head_cut, "start_marks": has_marks}
|
||||||
handle.seek(offset)
|
handle.seek(offset)
|
||||||
chunk = handle.read(min(size - offset, FOLLOW_READ_LIMIT))
|
chunk = handle.read(min(size - offset, FOLLOW_READ_LIMIT))
|
||||||
# Only whole lines are returned; an unfinished last line is left for the
|
# Only whole lines are returned; an unfinished last line is left for the
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"Tested by the community": "Von der Community getestet",
|
||||||
"# Alternative if you prefer screen": "# Alternative, wenn Sie den Bildschirm bevorzugen",
|
"# Alternative if you prefer screen": "# Alternative, wenn Sie den Bildschirm bevorzugen",
|
||||||
"# Recommended: avoids disconnection during upgrade": "# Empfohlen: Verhindert Verbindungsabbrüche während des Upgrades",
|
"# Recommended: avoids disconnection during upgrade": "# Empfohlen: Verhindert Verbindungsabbrüche während des Upgrades",
|
||||||
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Überprüfte Einträge werden entfernt. Deaktivieren Sie die Markierung, um sie in VM zu behalten.)",
|
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Überprüfte Einträge werden entfernt. Deaktivieren Sie die Markierung, um sie in VM zu behalten.)",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"Tested by the community": "Probado por la comunidad",
|
||||||
"# Alternative if you prefer screen": "# Alternativa si prefieres la pantalla",
|
"# Alternative if you prefer screen": "# Alternativa si prefieres la pantalla",
|
||||||
"# Recommended: avoids disconnection during upgrade": "# Recomendado: evita la desconexión durante la actualización",
|
"# Recommended: avoids disconnection during upgrade": "# Recomendado: evita la desconexión durante la actualización",
|
||||||
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Las entradas marcadas se eliminarán. Desmarque para mantener en VM).",
|
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Las entradas marcadas se eliminarán. Desmarque para mantener en VM).",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"Tested by the community": "Testé par la communauté",
|
||||||
"# Alternative if you prefer screen": "# Alternative si vous préférez l'écran",
|
"# Alternative if you prefer screen": "# Alternative si vous préférez l'écran",
|
||||||
"# Recommended: avoids disconnection during upgrade": "# Recommandé : évite la déconnexion lors de la mise à niveau",
|
"# Recommended: avoids disconnection during upgrade": "# Recommandé : évite la déconnexion lors de la mise à niveau",
|
||||||
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Les entrées cochées seront supprimées. Décochez pour conserver dans la VM.)",
|
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Les entrées cochées seront supprimées. Décochez pour conserver dans la VM.)",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"Tested by the community": "Testato dalla comunità",
|
||||||
"# Alternative if you prefer screen": "# Alternativa se preferisci lo schermo",
|
"# Alternative if you prefer screen": "# Alternativa se preferisci lo schermo",
|
||||||
"# Recommended: avoids disconnection during upgrade": "# Consigliato: evita la disconnessione durante l'aggiornamento",
|
"# Recommended: avoids disconnection during upgrade": "# Consigliato: evita la disconnessione durante l'aggiornamento",
|
||||||
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Le voci selezionate verranno rimosse. Deseleziona per mantenerle nella VM.)",
|
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Le voci selezionate verranno rimosse. Deseleziona per mantenerle nella VM.)",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"Tested by the community": "Testado pela comunidade",
|
||||||
"# Alternative if you prefer screen": "# Alternativa se você preferir tela",
|
"# Alternative if you prefer screen": "# Alternativa se você preferir tela",
|
||||||
"# Recommended: avoids disconnection during upgrade": "# Recomendado: evita desconexão durante a atualização",
|
"# Recommended: avoids disconnection during upgrade": "# Recomendado: evita desconexão durante a atualização",
|
||||||
"(Checked entries will be removed. Uncheck to keep in VM.)": "(As entradas marcadas serão removidas. Desmarque para manter na VM.)",
|
"(Checked entries will be removed. Uncheck to keep in VM.)": "(As entradas marcadas serão removidas. Desmarque para manter na VM.)",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"Tested by the community": "Otestované komunitou",
|
||||||
"# Alternative if you prefer screen": "# Alternatíva, ak preferujete screen",
|
"# Alternative if you prefer screen": "# Alternatíva, ak preferujete screen",
|
||||||
"# Recommended: avoids disconnection during upgrade": "# Odporúčané: zabráni odpojeniu počas aktualizácie",
|
"# Recommended: avoids disconnection during upgrade": "# Odporúčané: zabráni odpojeniu počas aktualizácie",
|
||||||
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Označené položky sa odstránia. Zrušte označenie, ak ich chcete ponechať vo VM.)",
|
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Označené položky sa odstránia. Zrušte označenie, ak ich chcete ponechať vo VM.)",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"Tested by the community": "Testad av communityn",
|
||||||
"# Alternative if you prefer screen": "# Alternativ om du föredrar skärm",
|
"# Alternative if you prefer screen": "# Alternativ om du föredrar skärm",
|
||||||
"# Recommended: avoids disconnection during upgrade": "# Rekommenderas: undviker frånkoppling under uppgradering",
|
"# Recommended: avoids disconnection during upgrade": "# Rekommenderas: undviker frånkoppling under uppgradering",
|
||||||
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Markerade poster kommer att tas bort. Avmarkera för att behålla i VM.)",
|
"(Checked entries will be removed. Uncheck to keep in VM.)": "(Markerade poster kommer att tas bort. Avmarkera för att behålla i VM.)",
|
||||||
|
|||||||
@@ -227,6 +227,12 @@ Generated templates start as `generated-unvalidated`. Promotion requires:
|
|||||||
5. Image replacement with persistent volumes preserved.
|
5. Image replacement with persistent volumes preserved.
|
||||||
6. Review of every platform adaptation and unsupported feature.
|
6. Review of every platform adaptation and unsupported feature.
|
||||||
|
|
||||||
|
Real tests are recorded in `catalog/verification.json`, which the catalog
|
||||||
|
applies on top of the generated templates and the overlays, so a regeneration
|
||||||
|
keeps them: `"status": "laboratory-validated"` for an application tested in
|
||||||
|
the ProxMenux lab, or `"community_tested": {"by": "<GitHub user>", "date":
|
||||||
|
"<YYYY-MM-DD>"}` for one tested by the community.
|
||||||
|
|
||||||
The mini changelog comes from the LinuxServer README `Versions` section. At
|
The mini changelog comes from the LinuxServer README `Versions` section. At
|
||||||
installation, the architecture-specific registry digest and image labels are
|
installation, the architecture-specific registry digest and image labels are
|
||||||
recorded back into the local app JSON for future update comparisons.
|
recorded back into the local app JSON for future update comparisons.
|
||||||
|
|||||||
@@ -506,5 +506,9 @@
|
|||||||
},
|
},
|
||||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||||
"automatic_unattended_updates": false
|
"automatic_unattended_updates": false
|
||||||
|
},
|
||||||
|
"community_tested": {
|
||||||
|
"by": "Vaso73",
|
||||||
|
"date": "2026-09-27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,5 +396,9 @@
|
|||||||
},
|
},
|
||||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||||
"automatic_unattended_updates": false
|
"automatic_unattended_updates": false
|
||||||
|
},
|
||||||
|
"community_tested": {
|
||||||
|
"by": "Vaso73",
|
||||||
|
"date": "2026-09-27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3644,6 +3644,10 @@
|
|||||||
"category_label": "Monitoring & Analytics",
|
"category_label": "Monitoring & Analytics",
|
||||||
"template": "apps/glances.json",
|
"template": "apps/glances.json",
|
||||||
"template_status": "generated-unvalidated",
|
"template_status": "generated-unvalidated",
|
||||||
|
"community_tested": {
|
||||||
|
"by": "Vaso73",
|
||||||
|
"date": "2026-09-27"
|
||||||
|
},
|
||||||
"automatic_install_candidate": true,
|
"automatic_install_candidate": true,
|
||||||
"untranslated_blockers": [],
|
"untranslated_blockers": [],
|
||||||
"requires_privileged_lxc": true,
|
"requires_privileged_lxc": true,
|
||||||
@@ -8548,6 +8552,10 @@
|
|||||||
"category_label": "Databases",
|
"category_label": "Databases",
|
||||||
"template": "apps/pocketbase.json",
|
"template": "apps/pocketbase.json",
|
||||||
"template_status": "generated-unvalidated",
|
"template_status": "generated-unvalidated",
|
||||||
|
"community_tested": {
|
||||||
|
"by": "Vaso73",
|
||||||
|
"date": "2026-09-27"
|
||||||
|
},
|
||||||
"automatic_install_candidate": true,
|
"automatic_install_candidate": true,
|
||||||
"untranslated_blockers": [],
|
"untranslated_blockers": [],
|
||||||
"requires_privileged_lxc": false,
|
"requires_privileged_lxc": false,
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Applications tested for real. \"status\": \"laboratory-validated\" when tested in the ProxMenux lab; \"community_tested\" with the tester's GitHub user and the date when tested by the community. Applied on top of the generated templates and the overlays, so a catalog regeneration keeps it.",
|
||||||
|
"applications": {
|
||||||
|
"2fauth": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"adguardhome-sync": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"albyhub": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"alist": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"amule": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"codeproject-ai": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"copyparty": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"crafty": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"ddclient": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"duplicati": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"etherpad": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"filebrowser-quantum": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"flaresolverr": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"flexget": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"frigate": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"community_tested": {
|
||||||
|
"by": "Vaso73",
|
||||||
|
"date": "2026-09-27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grafana": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"immich": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"jdownloader": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"jenkins": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"linkwarden": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"memos": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"mineos-node": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"motioneye": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"nextcloud": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"openlist": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"openssh-server": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"paperless-ngx": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"phpmyadmin": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"pocketbase": {
|
||||||
|
"community_tested": {
|
||||||
|
"by": "Vaso73",
|
||||||
|
"date": "2026-09-27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qbittorrent": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"rclone": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"rdtclient": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"snapotter": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"thelounge": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"trilium": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"wg-easy": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
},
|
||||||
|
"wireguard": {
|
||||||
|
"status": "laboratory-validated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,17 @@ import sys
|
|||||||
|
|
||||||
NO_LOGIN = ('nologin', 'false')
|
NO_LOGIN = ('nologin', 'false')
|
||||||
LOG_DIR = Path('/var/log/proxmenux/oci')
|
LOG_DIR = Path('/var/log/proxmenux/oci')
|
||||||
|
# Each start is marked in the console log by a pre-start hook. The hook runs
|
||||||
|
# the script only when it exists and always succeeds: a hook that fails would
|
||||||
|
# stop the container from starting.
|
||||||
|
START_MARK_SCRIPT = Path(__file__).resolve().with_name('oci_console_mark.sh')
|
||||||
|
START_MARK = '=== ProxMenux: container started '
|
||||||
|
|
||||||
|
|
||||||
|
def start_mark_hook(vmid: int) -> str:
|
||||||
|
script = START_MARK_SCRIPT
|
||||||
|
# `test`, not `[`: a bracket in the configuration reads as a snapshot section.
|
||||||
|
return f"lxc.hook.pre-start: /bin/sh -c 'test -x {script} && {script} {int(vmid)}; exit 0'"
|
||||||
LOGROTATE = Path('/etc/logrotate.d/proxmenux-oci')
|
LOGROTATE = Path('/etc/logrotate.d/proxmenux-oci')
|
||||||
# copytruncate, because liblxc keeps the file open for as long as the
|
# copytruncate, because liblxc keeps the file open for as long as the
|
||||||
# container runs; moving it away would leave the application writing into the
|
# container runs; moving it away would leave the application writing into the
|
||||||
@@ -165,8 +176,9 @@ def enable_log(vmid: int) -> Path:
|
|||||||
text = conf.read_text()
|
text = conf.read_text()
|
||||||
current, _, snapshots = text.partition('\n[')
|
current, _, snapshots = text.partition('\n[')
|
||||||
wanted = f'lxc.console.logfile: {path}'
|
wanted = f'lxc.console.logfile: {path}'
|
||||||
kept = [line for line in current.splitlines() if not line.startswith('lxc.console.logfile:')]
|
kept = [line for line in current.splitlines()
|
||||||
kept.append(wanted)
|
if not line.startswith('lxc.console.logfile:') and START_MARK_SCRIPT.name not in line]
|
||||||
|
kept += [wanted, start_mark_hook(vmid)]
|
||||||
rebuilt = '\n'.join(kept) + '\n'
|
rebuilt = '\n'.join(kept) + '\n'
|
||||||
if snapshots:
|
if snapshots:
|
||||||
rebuilt += '\n[' + snapshots
|
rebuilt += '\n[' + snapshots
|
||||||
@@ -176,6 +188,14 @@ def enable_log(vmid: int) -> Path:
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def remove_log(vmid: int) -> None:
|
||||||
|
"""Delete the console log of a removed container and its rotated copies."""
|
||||||
|
base = log_path(vmid)
|
||||||
|
for path in [base, *LOG_DIR.glob(f'{base.name}.*')]:
|
||||||
|
if path.is_file() and not path.is_symlink():
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
|
||||||
def configure(vmid: int) -> dict:
|
def configure(vmid: int) -> dict:
|
||||||
"""Console log and Proxmox terminal of a container being created."""
|
"""Console log and Proxmox terminal of a container being created."""
|
||||||
path = enable_log(vmid)
|
path = enable_log(vmid)
|
||||||
|
|||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Marks each start of an OCI container in its console log, so the log can be
|
||||||
|
# read from the last start. Run by the container's lxc.hook.pre-start.
|
||||||
|
case "$1" in
|
||||||
|
''|*[!0-9]*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
printf '\n=== ProxMenux: container started %s ===\n' "$(date '+%Y-%m-%d %H:%M:%S')" \
|
||||||
|
>> "/var/log/proxmenux/oci/$1.console.log" 2>/dev/null
|
||||||
|
exit 0
|
||||||
@@ -44,7 +44,9 @@ BASIC = {'arch', 'cmode', 'console', 'tty', 'cores', 'cpulimit', 'cpuunits', 'de
|
|||||||
'lxc.signal.halt', 'lxc.environment.runtime',
|
'lxc.signal.halt', 'lxc.environment.runtime',
|
||||||
# The container's console log, set by the installer on every
|
# The container's console log, set by the installer on every
|
||||||
# creation; the rebuilt container gets it again the same way.
|
# creation; the rebuilt container gets it again the same way.
|
||||||
'lxc.console.logfile'}
|
'lxc.console.logfile',
|
||||||
|
# The hook that marks each start in that log, set the same way.
|
||||||
|
'lxc.hook.pre-start'}
|
||||||
# Their output is data (and may hold saved secrets); it is never logged.
|
# Their output is data (and may hold saved secrets); it is never logged.
|
||||||
DATA_COMMANDS = {('pct', 'config'), ('pvesh', 'get')}
|
DATA_COMMANDS = {('pct', 'config'), ('pvesh', 'get')}
|
||||||
LOG_DIR = Path(os.environ.get('OCI_LOG_DIR', '/var/log/proxmenux/oci'))
|
LOG_DIR = Path(os.environ.get('OCI_LOG_DIR', '/var/log/proxmenux/oci'))
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import sys
|
|||||||
import oci_image_cache as image_cache
|
import oci_image_cache as image_cache
|
||||||
import oci_instances as instances
|
import oci_instances as instances
|
||||||
from oci_installation_state import parse_config
|
from oci_installation_state import parse_config
|
||||||
|
import oci_console
|
||||||
from oci_ui import translate, msg_info, msg_ok, msg_warn, msg_error
|
from oci_ui import translate, msg_info, msg_ok, msg_warn, msg_error
|
||||||
|
|
||||||
# The private networks ProxMenux creates for multi-container applications.
|
# The private networks ProxMenux creates for multi-container applications.
|
||||||
@@ -118,11 +119,13 @@ def remove(root, vmid):
|
|||||||
config = guest_config(member)
|
config = guest_config(member)
|
||||||
if config is None:
|
if config is None:
|
||||||
msg_warn(f"{translate('The container no longer exists:')} CT {member}")
|
msg_warn(f"{translate('The container no longer exists:')} CT {member}")
|
||||||
|
oci_console.remove_log(member)
|
||||||
elif instances.identity(config) != record['installation_id']:
|
elif instances.identity(config) != record['installation_id']:
|
||||||
msg_warn(f"{translate('The VMID belongs to another container now and is not touched:')} CT {member}")
|
msg_warn(f"{translate('The VMID belongs to another container now and is not touched:')} CT {member}")
|
||||||
else:
|
else:
|
||||||
subprocess.run(['pct', 'stop', str(member), '--skiplock', '1'], check=False, capture_output=True)
|
subprocess.run(['pct', 'stop', str(member), '--skiplock', '1'], check=False, capture_output=True)
|
||||||
run('pct', 'destroy', str(member), '--purge', '1', '--destroy-unreferenced-disks', '1')
|
run('pct', 'destroy', str(member), '--purge', '1', '--destroy-unreferenced-disks', '1')
|
||||||
|
oci_console.remove_log(member)
|
||||||
msg_ok(f"{translate('Container removed:')} CT {member}")
|
msg_ok(f"{translate('Container removed:')} CT {member}")
|
||||||
if bridge and not bridge_in_use(bridge, set(members)):
|
if bridge and not bridge_in_use(bridge, set(members)):
|
||||||
release_bridge(bridge)
|
release_bridge(bridge)
|
||||||
|
|||||||
@@ -23,6 +23,15 @@
|
|||||||
"status": {
|
"status": {
|
||||||
"enum": ["generated-unvalidated", "generated-review-required", "laboratory-validated", "stable"]
|
"enum": ["generated-unvalidated", "generated-review-required", "laboratory-validated", "stable"]
|
||||||
},
|
},
|
||||||
|
"community_tested": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["by"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"by": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9-]{0,38}$"},
|
||||||
|
"date": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}
|
||||||
|
}
|
||||||
|
},
|
||||||
"catalog_ui": {
|
"catalog_ui": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["title", "description", "category", "architectures", "launch", "mini_changelog"],
|
"required": ["title", "description", "category", "architectures", "launch", "mini_changelog"],
|
||||||
|
|||||||
@@ -414,6 +414,7 @@ class Catalog:
|
|||||||
ui = template["catalog_ui"]
|
ui = template["catalog_ui"]
|
||||||
item["hidden"] = overlay_ui.get("hidden", ui.get("hidden", False))
|
item["hidden"] = overlay_ui.get("hidden", ui.get("hidden", False))
|
||||||
item["template_status"] = template["status"]
|
item["template_status"] = template["status"]
|
||||||
|
self._index_community_tested(item, template)
|
||||||
item["automatic_install_candidate"] = compatibility[
|
item["automatic_install_candidate"] = compatibility[
|
||||||
"automatic_install_candidate"
|
"automatic_install_candidate"
|
||||||
]
|
]
|
||||||
@@ -639,6 +640,7 @@ class Catalog:
|
|||||||
item = index_items[app_id]
|
item = index_items[app_id]
|
||||||
item["template"] = f"apps/{app_id}.json"
|
item["template"] = f"apps/{app_id}.json"
|
||||||
item["template_status"] = template["status"]
|
item["template_status"] = template["status"]
|
||||||
|
self._index_community_tested(item, template)
|
||||||
item["automatic_install_candidate"] = template["compatibility"][
|
item["automatic_install_candidate"] = template["compatibility"][
|
||||||
"automatic_install_candidate"
|
"automatic_install_candidate"
|
||||||
]
|
]
|
||||||
@@ -792,6 +794,33 @@ class Catalog:
|
|||||||
apply_stack_support(template)
|
apply_stack_support(template)
|
||||||
from .gpu import apply_gpu_contract
|
from .gpu import apply_gpu_contract
|
||||||
apply_gpu_contract(template)
|
apply_gpu_contract(template)
|
||||||
|
# Last, so the stack compiler does not reset it.
|
||||||
|
self._apply_verification(app_id, template)
|
||||||
|
|
||||||
|
def _apply_verification(self, app_id: str, template: dict[str, Any]) -> None:
|
||||||
|
"""Real tests recorded in verification.json, kept across regenerations."""
|
||||||
|
path = self.catalog_dir / "verification.json"
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
entry = json.loads(path.read_text(encoding="utf-8")).get("applications", {}).get(app_id)
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
return
|
||||||
|
# An application that can no longer be installed is not shown as verified.
|
||||||
|
installable = template.get("compatibility", {}).get("automatic_install_candidate", False)
|
||||||
|
if entry.get("status") == "laboratory-validated" and installable:
|
||||||
|
template["status"] = "laboratory-validated"
|
||||||
|
if isinstance(entry.get("community_tested"), dict):
|
||||||
|
template["community_tested"] = dict(entry["community_tested"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _index_community_tested(item: dict[str, Any], template: dict[str, Any]) -> None:
|
||||||
|
"""Who tested the application for real outside the ProxMenux lab, and
|
||||||
|
when, as recorded in its overlay."""
|
||||||
|
tested = template.get("community_tested")
|
||||||
|
if isinstance(tested, dict) and tested.get("by"):
|
||||||
|
item["community_tested"] = {"by": str(tested["by"]), "date": str(tested.get("date") or "")}
|
||||||
|
else:
|
||||||
|
item.pop("community_tested", None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _deep_merge(cls, target: dict[str, Any], overlay: dict[str, Any]) -> None:
|
def _deep_merge(cls, target: dict[str, Any], overlay: dict[str, Any]) -> None:
|
||||||
|
|||||||
@@ -167,8 +167,13 @@ def _app_detail_text(catalog: Catalog, item: dict[str, Any], template: dict[str,
|
|||||||
lines.append(f"{label + ':':<17} {value}")
|
lines.append(f"{label + ':':<17} {value}")
|
||||||
|
|
||||||
row(translate("Source"), publisher(item))
|
row(translate("Source"), publisher(item))
|
||||||
row(translate("Status"), translate("Verified by ProxMenux") if is_tested(item)
|
# Only a real test is shown; OCI manager Apps is labelled beta as a whole.
|
||||||
else translate("Not yet verified by ProxMenux (beta)"))
|
community = item.get("community_tested") or {}
|
||||||
|
if is_tested(item):
|
||||||
|
row(translate("Status"), translate("Verified by ProxMenux"))
|
||||||
|
elif community.get("by"):
|
||||||
|
who = " · ".join(part for part in (f"@{community['by']}", community.get("date")) if part)
|
||||||
|
row(translate("Status"), f"{translate('Tested by the community')} ({who})")
|
||||||
row(translate("Architectures"), _display_architectures(ui))
|
row(translate("Architectures"), _display_architectures(ui))
|
||||||
endpoints = template.get("first_run", {}).get("endpoints", [])
|
endpoints = template.get("first_run", {}).get("endpoints", [])
|
||||||
if endpoints:
|
if endpoints:
|
||||||
|
|||||||
@@ -55,6 +55,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"p": "Selecting an application shows its description, the image it runs and two installation modes: <strong>Install with default settings</strong>, which asks almost nothing, and <strong>Install with advanced settings</strong>, which offers the real storage, bridge and resource selectors of the node."
|
"p": "Selecting an application shows its description, the image it runs and two installation modes: <strong>Install with default settings</strong>, which asks almost nothing, and <strong>Install with advanced settings</strong>, which offers the real storage, bridge and resource selectors of the node."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "An application that has been tested for real shows it in that view: <strong>Verified by ProxMenux</strong> when it was tested in the ProxMenux lab, marked with ✓ in the lists, or <strong>Tested by the community</strong>, with who tested it and when."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -150,6 +150,8 @@
|
|||||||
"items": [
|
"items": [
|
||||||
"The output is kept on the host in <code>/var/log/proxmenux/oci/VMID.console.log</code> (mode 0600), from the first start and across restarts, so it can be read with the container stopped.",
|
"The output is kept on the host in <code>/var/log/proxmenux/oci/VMID.console.log</code> (mode 0600), from the first start and across restarts, so it can be read with the container stopped.",
|
||||||
"The file is rotated at 10 MB, keeping three compressed copies (<code>/etc/logrotate.d/proxmenux-oci</code>).",
|
"The file is rotated at 10 MB, keeping three compressed copies (<code>/etc/logrotate.d/proxmenux-oci</code>).",
|
||||||
|
"Each start of the container is marked in the output. The tab shows the output since the last start by default; <strong>Full history</strong> also shows the previous ones.",
|
||||||
|
"When the application is removed, its output is removed with it.",
|
||||||
"The last 100, 500 or 1000 lines are shown. While the container runs, new lines are followed live; scrolling up pauses the follow, and <strong>Follow</strong> resumes it.",
|
"The last 100, 500 or 1000 lines are shown. While the container runs, new lines are followed live; scrolling up pauses the follow, and <strong>Follow</strong> resumes it.",
|
||||||
"A filter shows only the lines that contain a text, and <strong>Download</strong> saves the lines loaded.",
|
"A filter shows only the lines that contain a text, and <strong>Download</strong> saves the lines loaded.",
|
||||||
"Colour codes are removed and a line that a progress bar redraws is shown in its final state.",
|
"Colour codes are removed and a line that a progress bar redraws is shown in its final state.",
|
||||||
|
|||||||
@@ -55,6 +55,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"p": "Al seleccionar una aplicación se muestra su descripción, la imagen que ejecuta y dos modos de instalación: <strong>Instalar con la configuración predeterminada</strong>, que apenas hace preguntas, e <strong>Instalar con la configuración avanzada</strong>, que ofrece los selectores reales de almacenamiento, bridge y recursos del nodo."
|
"p": "Al seleccionar una aplicación se muestra su descripción, la imagen que ejecuta y dos modos de instalación: <strong>Instalar con la configuración predeterminada</strong>, que apenas hace preguntas, e <strong>Instalar con la configuración avanzada</strong>, que ofrece los selectores reales de almacenamiento, bridge y recursos del nodo."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": "Una aplicación probada en real lo indica en esa vista: <strong>Verificado por ProxMenux</strong> si se probó en el laboratorio de ProxMenux, marcada con ✓ en las listas, o <strong>Probado por la comunidad</strong>, con quién la probó y cuándo."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -150,6 +150,8 @@
|
|||||||
"items": [
|
"items": [
|
||||||
"La salida se guarda en el host en <code>/var/log/proxmenux/oci/VMID.console.log</code> (modo 0600), desde el primer arranque y entre reinicios, así que puede leerse con el contenedor detenido.",
|
"La salida se guarda en el host en <code>/var/log/proxmenux/oci/VMID.console.log</code> (modo 0600), desde el primer arranque y entre reinicios, así que puede leerse con el contenedor detenido.",
|
||||||
"El archivo se rota al llegar a 10 MB y se conservan tres copias comprimidas (<code>/etc/logrotate.d/proxmenux-oci</code>).",
|
"El archivo se rota al llegar a 10 MB y se conservan tres copias comprimidas (<code>/etc/logrotate.d/proxmenux-oci</code>).",
|
||||||
|
"Cada arranque del contenedor queda marcado en la salida. La pestaña muestra por defecto la salida desde el último arranque; <strong>Todo el historial</strong> muestra también los anteriores.",
|
||||||
|
"Al eliminar la aplicación, su salida se elimina con ella.",
|
||||||
"Se muestran las últimas 100, 500 o 1000 líneas. Con el contenedor en marcha, las líneas nuevas se siguen en directo; al desplazarse hacia arriba el seguimiento se pausa, y <strong>Seguir</strong> lo reanuda.",
|
"Se muestran las últimas 100, 500 o 1000 líneas. Con el contenedor en marcha, las líneas nuevas se siguen en directo; al desplazarse hacia arriba el seguimiento se pausa, y <strong>Seguir</strong> lo reanuda.",
|
||||||
"Un filtro muestra solo las líneas que contienen un texto, y <strong>Descargar</strong> guarda las líneas cargadas.",
|
"Un filtro muestra solo las líneas que contienen un texto, y <strong>Descargar</strong> guarda las líneas cargadas.",
|
||||||
"Los códigos de color se eliminan y una línea que redibuja una barra de progreso se muestra en su estado final.",
|
"Los códigos de color se eliminan y una línea que redibuja una barra de progreso se muestra en su estado final.",
|
||||||
|
|||||||
Reference in New Issue
Block a user