Compare commits

..
1 Commits
Author SHA1 Message Date
root 6e5bb5eac1 feat(ops): værtens apt-sikkerhedsopdateringer på driftssiden
CI / check (push) Successful in 32s
CD / deploy (push) Successful in 24s
Proxmox VE får 'nyeste' fra værtens apt-kandidat, ny sektion med
sikkerhedspakker/genstartsbehov, ntfy ved nye sikkerhedsopdateringer.
_norm stripper release-præfiks (qBittorrent).

Claude-Session: https://claude.ai/code/session_01AUj1LSMmrowcomkn1ciAgj
2026-10-09 19:25:57 +02:00
4 changed files with 37 additions and 5 deletions
+2 -1
View File
@@ -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)
+1 -1
View File
@@ -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'),
+21
View File
@@ -41,6 +41,27 @@
</table>
<p id="opsnomatch" class="muted" style="display:none;margin:.4rem 0">Ingen tjenester matcher filteret.</p>
{% if host %}
<h2 style="display:flex;align-items:baseline;gap:.8rem;flex-wrap:wrap">Proxmox-vært (apt)
{% if host.security %}<span class="vbadge eol">🔒 {{ host.security }} sikkerhedsopdatering{{ 'er' if host.security != 1 }}</span>
{% else %}<span class="vbadge ok">ingen sikkerhedsopdateringer</span>{% endif %}
{% if host.upgradable %}<span class="vbadge update">{{ host.upgradable }} pakker i alt</span>{% endif %}
{% if host.reboot_required %}<span class="vbadge soon" title="kører {{ host.kernel_running }}">genstart krævet{% if host.new_kernel %} (ny kerne){% endif %}</span>{% endif %}
</h2>
<p class="muted" style="margin:.2rem 0 .6rem">Kerne {{ host.kernel_running }}{% if host.new_kernel %} → {{ host.new_kernel }}{% endif %}
· apt-lister opdateret {{ (host.apt_checked or '?')|replace('T',' ') }}
· opdatér med <code>apt update &amp;&amp; apt dist-upgrade</code> på værten</p>
{% if host.security_pkgs %}
<details><summary class="muted" style="cursor:pointer">Vis sikkerhedspakker ({{ host.security }})</summary>
<table class="vtable" style="margin-top:.5rem">
<thead><tr><th>Pakke</th><th>Installeret</th><th>Ny</th></tr></thead>
<tbody>{% for p in host.security_pkgs %}
<tr><td data-label="Pakke"><strong>{{ p.name }}</strong></td><td class="font-mono" data-label="Installeret">{{ p['from'] or '—' }}</td><td class="font-mono muted" data-label="Ny">{{ p.to }}</td></tr>
{% endfor %}</tbody>
</table></details>
{% endif %}
{% endif %}
<h2>Static-assets</h2>
<div class="stat" style="max-width:18rem">
<div class="n">{{ version if version else '—' }}</div>
+13 -3
View File
@@ -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: