feat(oci): run official container images as native LXC containers

Adds the OCI manager: an engine that turns a Docker Compose file into an
LXC definition, a catalog of 365 applications drawn from LinuxServer.io
and other container image sources, and a per-instance registry recording
what each container was built from. Reachable from the main menu.

Catalog text is translated like every other string in the project: the
taglines go through translate() and land in lang/*.json, so the entries
read in all eight languages instead of only English.

Translation cache builder:
- a failed translation leaves the key absent rather than writing English,
  which previously made the string count as translated forever
- a result identical to a 3+ word source is rejected, catching a provider
  that silently returns the text it was given
- strings that are nothing but glossary terms keep their source spelling
  instead of being discarded as failures
- no backoff between attempts when the provider is deterministic
- application names are protected so "HAOS One" survives translation
- argos joins the provider list, and the workflow reads the OCI sources

Audit & Report:
- findings that moved in the wrong direction between runs are reported
  alongside the ones that improved
- an accepted risk can carry a review date and is flagged when it falls due
- backup checks explain in plain language what they looked at and what to
  do next

Monitor:
- disks can be excluded from periodic reads, and an idle disk says so
  instead of showing a stale temperature
- per-disk identity survives a controller or enclosure change
- scheduled Borg backups resolve their SSH key from the repository entry
- PVE upgrades log the package list and the resulting dpkg changes

The web build no longer copies scripts/ into public/: the documentation
links to GitHub, so nothing read that folder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-09-22 18:24:59 +02:00
co-authored by Claude Opus 5
parent b36498f215
commit bcabcb618c
670 changed files with 221410 additions and 215 deletions
+49 -1
View File
@@ -691,6 +691,37 @@ def run_assessment(profile: str = "full",
return run_id
def _scope(finding: dict) -> int:
"""How many objects a finding covers. A check that named three guests
and now names nine describes a larger problem, even at the same
gravity."""
return len(finding.get("affected") or [])
def _movement(previous: dict, current: dict) -> int:
"""Whether a finding present in both runs got worse (1), better (-1) or
held (0). Gravity decides; scope only breaks a tie, because a finding
takes the gravity of its gravest object and dropping from critical to
warning is progress however many objects it now names."""
before = audit_store.CLASS_ORDER.get(previous["classification"])
after = audit_store.CLASS_ORDER.get(current["classification"])
if before is not None and after is not None and before != after:
return 1 if after < before else -1
before_scope, after_scope = _scope(previous), _scope(current)
if after_scope != before_scope:
return 1 if after_scope > before_scope else -1
return 0
def _against(current: dict, previous: dict) -> dict:
"""A finding carrying where it came from, so the reader is told what
moved instead of only what it is now."""
return {**current,
"previous_classification": previous["classification"],
"previous_affected": _scope(previous),
"affected_count": _scope(current)}
def compare_runs(base_run: str, other_run: str) -> dict[str, list[dict]]:
"""Classify how findings moved between two runs.
@@ -700,6 +731,12 @@ def compare_runs(base_run: str, other_run: str) -> dict[str, list[dict]]:
that merges them would tell its reader the problem went away when the
decision was to live with it.
A finding that was already failing and still fails is never new. It
either got worse, got better without being resolved, or held: reporting
a warning that became critical as new hides that it was already there,
and reporting a critical that dropped to a warning as new tells the
reader their work created a problem.
``unchanged`` is kept so a report can state that the rest of the
surface held steady rather than leaving it unaccounted for.
"""
@@ -708,6 +745,7 @@ def compare_runs(base_run: str, other_run: str) -> dict[str, list[dict]]:
other = {f["check_id"]: f for f in audit_store.get_findings(other_run)}
new, resolved, accepted, unchanged, unverified = [], [], [], [], []
worse, better = [], []
for check_id, current in other.items():
previous = base.get(check_id)
was = previous["classification"] in problems if previous else False
@@ -718,8 +756,16 @@ def compare_runs(base_run: str, other_run: str) -> dict[str, list[dict]]:
unverified.append(current)
elif now and current.get("decision") == audit_store.DECISION_ACCEPTED:
accepted.append(current)
elif now and (not was or previous["classification"] != current["classification"]):
elif now and not was:
new.append(current)
elif now and was:
moved = _movement(previous, current)
if moved > 0:
worse.append(_against(current, previous))
elif moved < 0:
better.append(_against(current, previous))
else:
unchanged.append(current)
elif was and not now:
if current.get("decision") == audit_store.DECISION_ACCEPTED:
accepted.append(current)
@@ -738,6 +784,8 @@ def compare_runs(base_run: str, other_run: str) -> dict[str, list[dict]]:
return {
"new": new,
"worse": worse,
"better": better,
"resolved": resolved,
"accepted": accepted,
"unchanged": unchanged,