fix(monitor): register every OCI service port and read versions from the guest

This commit is contained in:
MacRimi
2026-09-28 17:05:52 +02:00
parent 8aade0e55c
commit 4a77d11bc4
6 changed files with 297 additions and 22 deletions
+24 -1
View File
@@ -111,11 +111,22 @@ interface AppConfig {
exclude_from_badge?: boolean exclude_from_badge?: boolean
} }
// A port as the installation record states it: the application answers on
// the first one, and any other is a service of its own (go2rtc next to Frigate).
interface PortDetail {
port: number
scheme: string | null
path: string | null
description: string
logo_url: string
}
interface DetectedApp { interface DetectedApp {
slug: string slug: string
name: string name: string
logo_url?: string | null logo_url?: string | null
default_ports?: number[] default_ports?: number[]
port_details?: PortDetail[] | null
// Categoría preset from helpers_cache.category_names[0] — used to // Categoría preset from helpers_cache.category_names[0] — used to
// auto-fill the Web Link editor when the user clicks "Register". // auto-fill the Web Link editor when the user clicks "Register".
category?: string | null category?: string | null
@@ -269,6 +280,7 @@ interface Suggestions {
web_path_hint: string | null web_path_hint: string | null
tracking_suggestion?: TrackingSuggestion | null tracking_suggestion?: TrackingSuggestion | null
default_ports?: number[] default_ports?: number[]
port_details?: PortDetail[] | null
logo_url?: string | null logo_url?: string | null
category?: string | null category?: string | null
extras?: DetectedApp[] extras?: DetectedApp[]
@@ -641,6 +653,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
name: suggestions.name_suggestion, name: suggestions.name_suggestion,
logo_url: suggestions.logo_url, logo_url: suggestions.logo_url,
default_ports: suggestions.default_ports, default_ports: suggestions.default_ports,
port_details: suggestions.port_details,
category: suggestions.category, category: suggestions.category,
tracking_suggestion: suggestions.tracking_suggestion, tracking_suggestion: suggestions.tracking_suggestion,
scheme: suggestions.oci_instance?.scheme === "https" ? "https" scheme: suggestions.oci_instance?.scheme === "https" ? "https"
@@ -844,7 +857,17 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
const suggestedPorts = isOciAdguard && p.slug === "image-adguard-home" const suggestedPorts = isOciAdguard && p.slug === "image-adguard-home"
? [80, ...(adguardSetupAvailable ? [3000] : [])] ? [80, ...(adguardSetupAvailable ? [3000] : [])]
: p.default_ports || [] : p.default_ports || []
if (p.slug !== "docker" && suggestedPorts.length) { if (p.port_details?.length && !(isOciAdguard && p.slug === "image-adguard-home")) {
seed.ports = p.port_details.map((d) => ({
port: d.port,
...(d.description ? { description: d.description } : {}),
scheme: d.scheme === "https" ? "https" as const
: d.scheme === "http" ? "http" as const : defaultSchemeFor(d.port),
web_path: d.path || s?.web_path_hint || "",
...(d.logo_url ? { logo_url: d.logo_url } : {}),
...(p.category ? { category: p.category } : {}),
}))
} else if (p.slug !== "docker" && suggestedPorts.length) {
seed.ports = suggestedPorts.map((port) => ({ seed.ports = suggestedPorts.map((port) => ({
port, port,
...(isOciAdguard && port === 3000 ? { description: "Config" } : {}), ...(isOciAdguard && port === 3000 ? { description: "Config" } : {}),
+119 -17
View File
@@ -5313,7 +5313,7 @@ def _helper_slug_meta(vmid) -> Optional[dict]:
_OCI_INSTANCE_ROOT = "/usr/local/share/proxmenux/oci/instances" _OCI_INSTANCE_ROOT = "/usr/local/share/proxmenux/oci/instances"
_OCI_CATALOG_INDEX = "/usr/local/share/proxmenux/oci/engine/catalog/index.json" _OCI_CATALOG_INDEX = "/usr/local/share/proxmenux/oci/engine/catalog/index.json"
_oci_catalog_cache: tuple[float, dict] | None = None _oci_catalog_cache: tuple[float, dict, dict] | None = None
def _oci_catalog_icons() -> dict: def _oci_catalog_icons() -> dict:
@@ -5325,26 +5325,72 @@ def _oci_catalog_icons() -> dict:
answered 404 — so the panel reads the catalog and keeps the record as the answered 404 — so the panel reads the catalog and keeps the record as the
fallback for an application the catalog no longer lists. fallback for an application the catalog no longer lists.
""" """
return _oci_catalog_entries()[0]
def _oci_catalog_entries() -> tuple[dict, dict]:
"""Icons and template files of the catalog, keyed by template id and by
application id."""
global _oci_catalog_cache global _oci_catalog_cache
now = time.time() now = time.time()
if _oci_catalog_cache and now - _oci_catalog_cache[0] < 600: if _oci_catalog_cache and now - _oci_catalog_cache[0] < 600:
return _oci_catalog_cache[1] return _oci_catalog_cache[1], _oci_catalog_cache[2]
icons: dict = {} icons: dict = {}
templates: dict = {}
try: try:
with open(_OCI_CATALOG_INDEX, encoding="utf-8") as handle: with open(_OCI_CATALOG_INDEX, encoding="utf-8") as handle:
for item in (json.load(handle) or {}).get("applications", []): for item in (json.load(handle) or {}).get("applications", []):
icon = (item or {}).get("icon") item = item or {}
if not isinstance(icon, str) or not icon.startswith("http"): icon = item.get("icon")
continue template = item.get("template")
# An installation records the template id; the catalog is keyed # An installation records the template id; the catalog is keyed
# by the application id and carries both. # by the application id and carries both.
for key in (item.get("template_id"), item.get("id")): for key in (item.get("template_id"), item.get("id")):
if key: if not key:
continue
if isinstance(icon, str) and icon.startswith("http"):
icons.setdefault(key, icon) icons.setdefault(key, icon)
if isinstance(template, str) and template.startswith("apps/"):
templates.setdefault(key, template)
except (OSError, ValueError, TypeError): except (OSError, ValueError, TypeError):
pass pass
_oci_catalog_cache = (now, icons) _oci_catalog_cache = (now, icons, templates)
return icons return icons, templates
def _oci_catalog_first_run(template_id) -> dict:
"""The first_run contract of the current catalog template. An existing
install keeps a copy from the day it was created; what the catalog added
since, such as a service icon, is read here."""
template = _oci_catalog_entries()[1].get(template_id or "")
if not template:
return {}
try:
with open(os.path.join(os.path.dirname(_OCI_CATALOG_INDEX), template), encoding="utf-8") as handle:
first_run = (json.load(handle) or {}).get("first_run")
except (OSError, ValueError, TypeError, AttributeError):
return {}
return first_run if isinstance(first_run, dict) else {}
def _oci_catalog_endpoint_icons(template_id) -> dict:
"""Icon of each service a catalog application serves on a port of its own,
keyed by port."""
endpoints = _oci_catalog_first_run(template_id).get("endpoints") or []
return {entry["port"]: entry["icon"] for entry in endpoints
if isinstance(entry, dict) and isinstance(entry.get("port"), int)
and isinstance(entry.get("icon"), str) and entry["icon"].startswith("https://")}
# "Web UI 2" names no service: the LinuxServer templates list the same
# interface over http and https, or a port devices report to, that way.
_OCI_GENERIC_ENDPOINT = re.compile(r"(web\s*ui|ui)?\s*\d*", re.IGNORECASE)
def _oci_service_name(label) -> str:
"""The service an endpoint label names: "go2rtc WebUI" is go2rtc."""
label = str(label or "").strip()
return re.sub(r"\s*(web\s*ui|ui)$", "", label, flags=re.IGNORECASE).strip() or label
def _oci_localised(value) -> str: def _oci_localised(value) -> str:
@@ -5433,20 +5479,27 @@ def ensure_oci_registration(vmid) -> bool:
meta = _oci_instance_meta(vmid) meta = _oci_instance_meta(vmid)
if not meta or not meta.get("name") or not _NAME_RE.match(str(meta["name"])): if not meta or not meta.get("name") or not _NAME_RE.match(str(meta["name"])):
return False return False
port = meta.get("endpoint_port") or next(iter(meta.get("ports") or []), None) category = meta.get("category_label") or meta.get("category") or ""
endpoints = meta.get("endpoints") or []
if not endpoints:
port = meta.get("endpoint_port") or next(iter(meta.get("ports") or []), None)
endpoints = [{"port": port, "scheme": meta.get("endpoint_scheme"), "path": meta.get("endpoint_path"),
"description": "", "logo_url": ""}] if isinstance(port, int) else []
ports = []
for endpoint in endpoints:
entry = {"port": endpoint["port"], "scheme": endpoint.get("scheme") or "http",
"web_path": endpoint.get("path") or "/", "category": category,
"description": endpoint.get("description") or ""}
if endpoint.get("logo_url"):
entry["logo_url"] = endpoint["logo_url"]
ports.append(entry)
payload = { payload = {
"name": meta["name"], "name": meta["name"],
"installed_via": "oci_image", "installed_via": "oci_image",
"helper_slug": meta.get("template_id") or "", "helper_slug": meta.get("template_id") or "",
"logo_url": meta.get("logo") or "", "logo_url": meta.get("logo") or "",
"update_method": "none", "update_method": "none",
"ports": [{ "ports": ports,
"port": port,
"scheme": meta.get("endpoint_scheme") or "http",
"web_path": meta.get("endpoint_path") or "/",
"category": meta.get("category_label") or meta.get("category") or "",
"description": "",
}] if isinstance(port, int) else [],
} }
# Registrations made at startup would each announce their update on their # Registrations made at startup would each announce their update on their
# own; the scheduled sweep sends pending updates together instead. # own; the scheduled sweep sends pending updates together instead.
@@ -5545,6 +5598,29 @@ def _oci_instance_meta(vmid) -> Optional[dict]:
if 1 <= port <= 65535 and port not in ports: if 1 <= port <= 65535 and port not in ports:
ports.append(port) ports.append(port)
template_id = str(template.get("id") or "").strip() template_id = str(template.get("id") or "").strip()
# The application is reached on its first endpoint and carries its own
# name and logo. Any other endpoint is a service of its own, such as
# go2rtc next to Frigate, and is described by its name and icon.
endpoints = []
endpoint_icons = None
for entry in (template.get("first_run") or {}).get("endpoints") or []:
port = entry.get("port") if isinstance(entry, dict) else None
if not isinstance(port, int) or not 1 <= port <= 65535 or port in [e["port"] for e in endpoints]:
continue
detail = {"port": port,
"scheme": str(entry.get("scheme") or "").strip().lower() or None,
"path": str(entry.get("path") or "").strip() or None,
"description": "", "logo_url": ""}
if endpoints:
if _OCI_GENERIC_ENDPOINT.fullmatch(str(entry.get("label") or "").strip()):
continue
if endpoint_icons is None:
endpoint_icons = _oci_catalog_endpoint_icons(template_id)
icon = entry.get("icon") if isinstance(entry.get("icon"), str) else ""
icon = endpoint_icons.get(port) or icon
detail["description"] = _oci_service_name(entry.get("label"))
detail["logo_url"] = icon if icon.startswith("https://") else ""
endpoints.append(detail)
catalog_icons = _oci_catalog_icons() catalog_icons = _oci_catalog_icons()
logo = catalog_icons.get(template_id) or "" logo = catalog_icons.get(template_id) or ""
if not logo: if not logo:
@@ -5575,6 +5651,7 @@ def _oci_instance_meta(vmid) -> Optional[dict]:
"endpoint_scheme": str(endpoint.get("scheme") or "").strip().lower() or None, "endpoint_scheme": str(endpoint.get("scheme") or "").strip().lower() or None,
"endpoint_path": str(endpoint.get("path") or "").strip() or None, "endpoint_path": str(endpoint.get("path") or "").strip() or None,
"ports": ports, "ports": ports,
"endpoints": endpoints,
# The exact image this container was created from. Its digest is what # The exact image this container was created from. Its digest is what
# an update is decided on; the version label is only for reading. # an update is decided on; the version label is only for reading.
"installed_digest": str(observed_image.get("manifest_digest") or "").strip() or None, "installed_digest": str(observed_image.get("manifest_digest") or "").strip() or None,
@@ -5677,6 +5754,10 @@ def _oci_image_versions(vmid, known: Optional[dict] = None, with_latest: bool =
result["image_created"] = installed.get("created") result["image_created"] = installed.get("created")
except Exception as exc: except Exception as exc:
return {**result, "error": f"could not read the installed image: {exc}"} return {**result, "error": f"could not read the installed image: {exc}"}
if not result.get("installed_version"):
# The container runs the installed digest, so what it states is the
# version of that image; once read it is kept with the digest.
result["installed_version"] = _oci_guest_version(vmid, meta.get("template_id"))
if not with_latest: if not with_latest:
return result return result
try: try:
@@ -5693,6 +5774,18 @@ def _oci_image_versions(vmid, known: Optional[dict] = None, with_latest: bool =
return result return result
def _oci_guest_version(vmid, template_id) -> Optional[str]:
"""The application version for an image that publishes none, read from
the file the catalog template names. Frigate states it only in
/opt/frigate/frigate/version.py. Needs the container running."""
spec = _oci_catalog_first_run(template_id).get("version_file") or {}
path, pattern = spec.get("path"), spec.get("regex")
if not (isinstance(path, str) and path.startswith("/") and isinstance(pattern, str)):
return None
rc, out, _err = _pct_exec(vmid, ["cat", path])
return _extract_version(out, pattern) if rc == 0 else None
def _oci_image_label(version: Optional[str], created: Optional[str], digest: Optional[str]) -> str: def _oci_image_label(version: Optional[str], created: Optional[str], digest: Optional[str]) -> str:
"""One image, as a line a reader can compare: version, build date, digest.""" """One image, as a line a reader can compare: version, build date, digest."""
parts = [version] if version else [] parts = [version] if version else []
@@ -6029,6 +6122,7 @@ def get_suggestions(vmid, force: bool = False) -> dict:
"tracking_suggestion": det_tracking, "tracking_suggestion": det_tracking,
}) })
port_details = None
if oci_meta: if oci_meta:
# The record states what this container runs, so a probe finding is # The record states what this container runs, so a probe finding is
# noise: CT 152 runs Chromium and ships a docker client, and offering # noise: CT 152 runs Chromium and ships a docker client, and offering
@@ -6042,7 +6136,11 @@ def get_suggestions(vmid, force: bool = False) -> dict:
name_sug = oci_meta["name"] or name_sug name_sug = oci_meta["name"] or name_sug
logo_url = oci_meta["logo"] or logo_url logo_url = oci_meta["logo"] or logo_url
category_suggestion = oci_meta["category_label"] or suggest_category_for(slug) category_suggestion = oci_meta["category_label"] or suggest_category_for(slug)
if oci_meta["endpoint_port"]: if oci_meta["endpoints"]:
port_details = oci_meta["endpoints"]
default_ports = [e["port"] for e in port_details]
ports = default_ports + [p for p in (oci_meta["ports"] or ports) if p not in default_ports]
elif oci_meta["endpoint_port"]:
default_ports = [oci_meta["endpoint_port"]] default_ports = [oci_meta["endpoint_port"]]
ports = [oci_meta["endpoint_port"]] + [p for p in (oci_meta["ports"] or ports) ports = [oci_meta["endpoint_port"]] + [p for p in (oci_meta["ports"] or ports)
if p != oci_meta["endpoint_port"]] if p != oci_meta["endpoint_port"]]
@@ -6053,6 +6151,7 @@ def get_suggestions(vmid, force: bool = False) -> dict:
# The first-run endpoint disappears once setup switches to :80. # The first-run endpoint disappears once setup switches to :80.
default_ports = [80] default_ports = [80]
ports = [80, 3000] ports = [80, 3000]
port_details = None
# Version tracking comes with the registration. Its updates are # Version tracking comes with the registration. Its updates are
# decided by the image, which always has a build date and a digest, # decided by the image, which always has a build date and a digest,
# so it applies even to an image that states no application version. # so it applies even to an image that states no application version.
@@ -6077,6 +6176,9 @@ def get_suggestions(vmid, force: bool = False) -> dict:
"web_path_hint": web_hint, "web_path_hint": web_hint,
"tracking_suggestion": tracking, "tracking_suggestion": tracking,
"default_ports": default_ports, "default_ports": default_ports,
# Scheme, path, description and icon of each suggested port, when the
# installation record states them.
"port_details": port_details,
"logo_url": logo_url or None, "logo_url": logo_url or None,
# Identity of a ProxMenux OCI install: the scheme the endpoint is # Identity of a ProxMenux OCI install: the scheme the endpoint is
# served on, the image it was created from, and the upstream source. # served on, the image it was created from, and the upstream source.
@@ -0,0 +1,129 @@
import json
import sys
import tempfile
from pathlib import Path
import unittest
from unittest.mock import patch
SCRIPTS = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SCRIPTS))
import lxc_apps
ICON = 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/go2rtc.webp'
VERSION_FILE = {'path': '/opt/frigate/frigate/version.py', 'regex': 'VERSION = "([0-9]+(?:\\.[0-9]+)+)'}
ENDPOINTS = [
{'label': 'Frigate WebUI', 'scheme': 'http', 'port': 5000, 'path': '/', 'source': 'x'},
{'label': 'go2rtc WebUI', 'scheme': 'http', 'port': 1984, 'path': '/', 'source': 'x'},
]
class CatalogFixture(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.root = Path(tmp.name)
(self.root / 'catalog/apps').mkdir(parents=True)
(self.root / 'catalog/index.json').write_text(json.dumps({'applications': [
{'id': 'frigate', 'template_id': 'image-frigate', 'template': 'apps/frigate.json',
'icon': 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/frigate.webp'}]}))
catalog_endpoints = [dict(ENDPOINTS[0]), dict(ENDPOINTS[1], icon=ICON)]
(self.root / 'catalog/apps/frigate.json').write_text(json.dumps(
{'id': 'image-frigate', 'first_run': {'endpoints': catalog_endpoints, 'version_file': VERSION_FILE}}))
patches = [patch.object(lxc_apps, '_OCI_INSTANCE_ROOT', str(self.root / 'instances')),
patch.object(lxc_apps, '_OCI_CATALOG_INDEX', str(self.root / 'catalog/index.json')),
patch.object(lxc_apps, '_oci_catalog_cache', None)]
for item in patches:
item.start()
self.addCleanup(item.stop)
def install(self, endpoints, template_id='image-frigate'):
(self.root / 'instances/190').mkdir(parents=True)
(self.root / 'instances/190/oci-compose.json').write_text(json.dumps({
'vmid': 190, 'status': 'installed', 'installation_id': 'install-190',
'observed': {'image': {'manifest_digest': 'sha256:' + 'b1' * 32, 'architecture': 'amd64'}},
'template': {'id': template_id, 'catalog_ui': {'title': 'Frigate'},
'first_run': {'endpoints': endpoints},
'container_contract': {'image': {'reference': 'ghcr.io/blakeblackshear/frigate:stable'},
'ports': [{'container_port': 8971}]}}}))
return lxc_apps._oci_instance_meta(190)
class OciEndpointTests(CatalogFixture):
def test_second_endpoint_is_its_own_service_with_the_current_catalog_icon(self):
# The record was written before the catalog gave go2rtc an icon.
meta = self.install(ENDPOINTS)
self.assertEqual(meta['endpoints'], [
{'port': 5000, 'scheme': 'http', 'path': '/', 'description': '', 'logo_url': ''},
{'port': 1984, 'scheme': 'http', 'path': '/', 'description': 'go2rtc', 'logo_url': ICON},
])
self.assertEqual(meta['endpoint_port'], 5000)
def test_single_endpoint_and_unknown_template(self):
meta = self.install([ENDPOINTS[0]], template_id='image-other')
self.assertEqual(meta['endpoints'], [
{'port': 5000, 'scheme': 'http', 'path': '/', 'description': '', 'logo_url': ''}])
def test_generic_second_endpoint_is_not_a_service(self):
# LinuxServer lists the same interface over http and https as "Web UI 1" and "Web UI 2".
meta = self.install([dict(ENDPOINTS[0], label='Web UI 1', port=3000),
dict(ENDPOINTS[0], label='Web UI 2', port=3001, scheme='https')],
template_id='image-krita')
self.assertEqual([e['port'] for e in meta['endpoints']], [3000])
def test_service_name_from_label(self):
for label, name in [('go2rtc WebUI', 'go2rtc'), ('Admin Web UI', 'Admin'), ('Setup UI', 'Setup'),
('Dashboard', 'Dashboard'), ('WebUI', 'WebUI'), ('', '')]:
with self.subTest(label=label):
self.assertEqual(lxc_apps._oci_service_name(label), name)
def test_suggestion_offers_every_endpoint(self):
self.install(ENDPOINTS)
with patch.object(lxc_apps, '_probe_listening_ports', return_value=[]), \
patch.object(lxc_apps, '_helper_slug_meta', return_value=None), \
patch.object(lxc_apps, '_fetch_tracking_hints', return_value={}), \
patch.object(lxc_apps, '_oci_image_versions', return_value={'error': 'offline'}), \
patch.object(lxc_apps, '_read_sidecar', return_value=None):
suggestion = lxc_apps.get_suggestions(190)
self.assertEqual(suggestion['default_ports'], [5000, 1984])
self.assertEqual([d['description'] for d in suggestion['port_details']], ['', 'go2rtc'])
self.assertEqual(suggestion['port_details'][1]['logo_url'], ICON)
def test_automatic_registration_registers_every_endpoint(self):
self.install(ENDPOINTS)
with patch.object(lxc_apps, '_read_sidecar', return_value=None), \
patch.object(lxc_apps, '_oci_dismissed', return_value={}), \
patch.object(lxc_apps, 'add_app', return_value=(True, {})) as add:
self.assertTrue(lxc_apps.ensure_oci_registration(190))
ports = add.call_args.args[1]['ports']
self.assertEqual([(p['port'], p['description']) for p in ports], [(5000, ''), (1984, 'go2rtc')])
self.assertNotIn('logo_url', ports[0])
self.assertEqual(ports[1]['logo_url'], ICON)
class OciGuestVersionTests(CatalogFixture):
def test_version_read_from_the_file_the_catalog_names(self):
with patch.object(lxc_apps, '_pct_exec', return_value=(0, 'VERSION = "0.18.0-77a66e7"\n', '')) as run:
self.assertEqual(lxc_apps._oci_guest_version(190, 'image-frigate'), '0.18.0')
run.assert_called_once_with(190, ['cat', '/opt/frigate/frigate/version.py'])
def test_stopped_container_and_template_without_version_file(self):
with patch.object(lxc_apps, '_pct_exec', return_value=(1, '', 'CT 190 not running')):
self.assertIsNone(lxc_apps._oci_guest_version(190, 'image-frigate'))
with patch.object(lxc_apps, '_pct_exec') as run:
self.assertIsNone(lxc_apps._oci_guest_version(190, 'image-other'))
run.assert_not_called()
def test_image_without_version_label_uses_the_guest(self):
self.install(ENDPOINTS)
# A cached answer without a version must not stop the guest read.
known = {'installed_digest': 'sha256:' + 'b1' * 32, 'installed_version': None, 'image_created': '2026-09-06'}
with patch.object(lxc_apps, '_oci_operation_running', return_value=False), \
patch.object(lxc_apps, '_oci_state_module', return_value=object()), \
patch.object(lxc_apps, '_pct_exec', return_value=(0, 'VERSION = "0.18.0-77a66e7"', '')):
result = lxc_apps._oci_image_versions(190, known=known, with_latest=False)
self.assertEqual(result['installed_version'], '0.18.0')
self.assertEqual(result['image_created'], '2026-09-06')
if __name__ == '__main__':
unittest.main()
+6 -1
View File
@@ -333,9 +333,14 @@
"scheme": "http", "scheme": "http",
"port": 1984, "port": 1984,
"path": "/", "path": "/",
"source": "integrated-go2rtc-native-oci-endpoint" "source": "integrated-go2rtc-native-oci-endpoint",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/go2rtc.webp"
} }
], ],
"version_file": {
"path": "/opt/frigate/frigate/version.py",
"regex": "VERSION = \"([0-9]+(?:\\.[0-9]+)+)"
},
"credentials": [] "credentials": []
}, },
"proxmox": { "proxmox": {
+6 -1
View File
@@ -333,9 +333,14 @@
"scheme": "http", "scheme": "http",
"port": 1984, "port": 1984,
"path": "/", "path": "/",
"source": "integrated-go2rtc-native-oci-endpoint" "source": "integrated-go2rtc-native-oci-endpoint",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/go2rtc.webp"
} }
], ],
"version_file": {
"path": "/opt/frigate/frigate/version.py",
"regex": "VERSION = \"([0-9]+(?:\\.[0-9]+)+)"
},
"credentials": [] "credentials": []
}, },
"proxmox": { "proxmox": {
+13 -2
View File
@@ -29,7 +29,8 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"by": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9-]{0,38}$"}, "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}$"} "date": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"},
"report": {"type": "string", "pattern": "^https://github\\.com/MacRimi/ProxMenux/(issues/[0-9]+|discussions/[0-9]+(#discussioncomment-[0-9]+)?)$"}
} }
}, },
"catalog_ui": { "catalog_ui": {
@@ -216,11 +217,21 @@
"scheme": {"enum": ["http", "https"]}, "scheme": {"enum": ["http", "https"]},
"port": {"type": "integer", "minimum": 1, "maximum": 65535}, "port": {"type": "integer", "minimum": 1, "maximum": 65535},
"path": {"type": "string", "pattern": "^/"}, "path": {"type": "string", "pattern": "^/"},
"source": {"type": "string"} "source": {"type": "string"},
"icon": {"type": "string", "pattern": "^https://"}
}, },
"additionalProperties": false "additionalProperties": false
} }
}, },
"version_file": {
"type": "object",
"required": ["path", "regex"],
"properties": {
"path": {"type": "string", "pattern": "^/"},
"regex": {"type": "string"}
},
"additionalProperties": false
},
"credentials": { "credentials": {
"type": "array", "type": "array",
"items": { "items": {