diff --git a/main.py b/main.py
index 81b3230..0b89de4 100644
--- a/main.py
+++ b/main.py
@@ -245,7 +245,8 @@ async def ops_content(request: Request):
version, rp, vers = await asyncio.gather(
svc.assets_version(), svc.rp_status(), versions.all_versions())
return templates.TemplateResponse(request, "admin_ops_body.html", {
- "request": request, "version": version, "rp": rp, "versions": vers})
+ "request": request, "version": version, "rp": rp, "versions": vers,
+ "host": versions.host_updates()})
@app.get("/admin/stats", response_class=HTMLResponse)
diff --git a/templates/admin_ops.html b/templates/admin_ops.html
index 5e78a6e..1dc2045 100644
--- a/templates/admin_ops.html
+++ b/templates/admin_ops.html
@@ -97,7 +97,7 @@
var fstate = { q:'', mode:'all', level:'all' };
// Filter ligger i shell (persistent) og genanvendes efter hver poll → fokus/værdi bevares.
function applyFilter(){
- var rows = box.querySelectorAll('.vtable tbody tr'), shown = 0;
+ var rows = box.querySelectorAll('.vtable tbody tr[data-name]'), shown = 0;
rows.forEach(function(tr){
var name = tr.getAttribute('data-name') || '',
st = tr.getAttribute('data-status'), lvl = tr.getAttribute('data-level'),
diff --git a/templates/admin_ops_body.html b/templates/admin_ops_body.html
index f65cdec..952b938 100644
--- a/templates/admin_ops_body.html
+++ b/templates/admin_ops_body.html
@@ -41,6 +41,27 @@
Ingen tjenester matcher filteret.
+{% if host %}
+Proxmox-vært (apt)
+ {% if host.security %}🔒 {{ host.security }} sikkerhedsopdatering{{ 'er' if host.security != 1 }}
+ {% else %}ingen sikkerhedsopdateringer{% endif %}
+ {% if host.upgradable %}{{ host.upgradable }} pakker i alt{% endif %}
+ {% if host.reboot_required %}genstart krævet{% if host.new_kernel %} (ny kerne){% endif %}{% endif %}
+
+Kerne {{ host.kernel_running }}{% if host.new_kernel %} → {{ host.new_kernel }}{% endif %}
+ · apt-lister opdateret {{ (host.apt_checked or '?')|replace('T',' ') }}
+ · opdatér med apt update && apt dist-upgrade på værten
+{% if host.security_pkgs %}
+Vis sikkerhedspakker ({{ host.security }})
+
+ | Pakke | Installeret | Ny |
+ {% for p in host.security_pkgs %}
+ | {{ p.name }} | {{ p['from'] or '—' }} | {{ p.to }} |
+ {% endfor %}
+
+{% endif %}
+{% endif %}
+
Static-assets
{{ version if version else '—' }}
diff --git a/versions.py b/versions.py
index ee1894e..6741a8f 100644
--- a/versions.py
+++ b/versions.py
@@ -74,8 +74,8 @@ SERVICES = [
def _norm(v):
if not v:
return ""
- # behold dato-versioner (fx NPMplus 2026-04-21-r2) — strip kun v-præfiks + build-metadata
- return re.sub(r"^[vV]", "", str(v).strip()).split("+")[0].strip().lower()
+ # behold dato-versioner (fx NPMplus 2026-04-21-r2) — strip kun v-/release-præfiks + build-metadata
+ return re.sub(r"^(release-)?[vV]?", "", str(v).strip()).split("+")[0].strip().lower()
async def _latest(src, client, store):
@@ -229,7 +229,8 @@ async def _one(name, kind, repo, client, local, store):
cur, lat_builtin = await _current(kind, client)
if not cur:
cur = local.get(name) # vært-indsamlet snapshot for LXC-tjenester
- latest = lat_builtin or await _latest(repo, client, store)
+ # _latest i snapshot'et = nyeste fra værten selv (fx apt-kandidat for pve-manager)
+ latest = lat_builtin or await _latest(repo, client, store) or local.get("_latest", {}).get(name)
eol = await _eol(name, cur, client)
level = None
if cur and latest:
@@ -263,6 +264,10 @@ async def _maybe_alert(res, client):
if e and e.get("state") in ("eol", "soon"):
sig = f"eol:{v['name']}:{e.get('date')}"
cur.add(sig); detail[sig] = f"⏳ {v['name']} EOL {e.get('date')}"
+ h = host_updates()
+ if h and h.get("security"):
+ sig = "sec:host:" + ",".join(sorted(f"{p['name']}={p['to']}" for p in h["security_pkgs"]))
+ cur.add(sig); detail[sig] = f"🔒 Proxmox-vært: {h['security']} sikkerhedsopdateringer"
try:
stored = set(json.loads(ALERT_STATE.read_text("utf-8")))
except Exception:
@@ -285,6 +290,11 @@ async def _maybe_alert(res, client):
ALERT_STATE.write_text(json.dumps(sorted(cur)), "utf-8")
+def host_updates():
+ """Værtens apt-status (indsamlet af portal-versions.py) eller None."""
+ return _load_local().get("_host")
+
+
async def all_versions():
hit = cache.get("ver:all")
if hit is not None: