Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e5bb5eac1 | ||
|
|
79f49bda45 |
@@ -43,6 +43,9 @@ app.add_middleware(SessionMiddleware, secret_key=config.SECRET_KEY, https_only=T
|
||||
same_site="lax", max_age=60 * 60 * 12)
|
||||
app.mount("/static", StaticFiles(directory=config.BASE / "static"), name="static")
|
||||
templates = Jinja2Templates(directory=str(config.BASE / "templates"))
|
||||
# Cache-busting af portalens eget CSS: uden Cache-Control cacher Safari heuristisk i timevis,
|
||||
# så en rettelse i portal.css nåede ikke telefonen. Versionen er filens ændringstid.
|
||||
templates.env.globals["static_v"] = str(int((config.BASE / "static" / "portal.css").stat().st_mtime))
|
||||
|
||||
oauth = OAuth()
|
||||
oauth.register(
|
||||
@@ -242,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)
|
||||
|
||||
+6
-7
@@ -174,11 +174,10 @@ th { color:var(--muted); font-weight:600; font-size:.8rem; text-transform:upperc
|
||||
.ucard.hidden { display:none; }
|
||||
|
||||
|
||||
/* Tabeller og andet bredt indhold må aldrig give siden vandret scroll på en telefon.
|
||||
Standard for alle sider: på smalle skærme bliver en tabel sit eget rulleområde,
|
||||
og lange ord/nøgler brydes. (Ops-siden har desuden sin egen kort-visning.) */
|
||||
/* Tabeller og andet bredt indhold må aldrig give siden vandret scroll.
|
||||
base.html pakker hver tabel i .wt-table-wrap (standardkomponent, også for injiceret indhold):
|
||||
tabellen ruller i sit eget område. Lange ord/nøgler uden for tabeller brydes. */
|
||||
.wt-main { overflow-wrap: anywhere; min-width: 0; }
|
||||
@media (max-width: 760px) {
|
||||
.wt-main table { display: block; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
.wt-main table td, .wt-main table th { overflow-wrap: normal; }
|
||||
}
|
||||
.wt-table-wrap { overflow-x: auto; max-width: 100%; -webkit-overflow-scrolling: touch; }
|
||||
.wt-table-wrap > table { margin-top: 0; }
|
||||
.wt-table-wrap table td, .wt-table-wrap table th { overflow-wrap: normal; }
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 && 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>
|
||||
|
||||
+17
-1
@@ -11,7 +11,7 @@
|
||||
<script>(()=>{try{var t=localStorage.getItem('theme')||(matchMedia('(prefers-color-scheme: light)').matches?'light':'dark');document.documentElement.dataset.theme=t}catch(e){document.documentElement.dataset.theme='dark'}})()</script>
|
||||
<!-- shared design system (SSOT: agw/Weircon-wind) hosted on assets.weircon.dk -->
|
||||
<link rel="stylesheet" href="https://assets.weircon.dk/css/weirsoe-theme.css" />
|
||||
<link rel="stylesheet" href="/static/portal.css" />
|
||||
<link rel="stylesheet" href="/static/portal.css?v={{ static_v }}" />
|
||||
<script defer src="https://assets.weircon.dk/js/weirsoe-theme.js"></script>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
@@ -56,6 +56,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Standardkomponent: enhver tabel i hovedindholdet får et rulleområde om sig (.wt-table-wrap),
|
||||
// så en bred tabel ruller i sig selv i stedet for at skubbe siden ud over kanten. Gælder også
|
||||
// indhold, der injiceres senere (fx /admin/ops).
|
||||
(function () {
|
||||
function pak(rod) {
|
||||
(rod || document).querySelectorAll('main table').forEach(function (t) {
|
||||
if (t.parentElement && t.parentElement.classList.contains('wt-table-wrap')) return;
|
||||
var w = document.createElement('div'); w.className = 'wt-table-wrap';
|
||||
t.parentNode.insertBefore(w, t); w.appendChild(t);
|
||||
});
|
||||
}
|
||||
window.wtPakTabeller = pak;
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { pak(); }); else pak();
|
||||
var main = document.querySelector('main');
|
||||
if (main && window.MutationObserver) new MutationObserver(function () { pak(main); }).observe(main, { childList: true, subtree: true });
|
||||
})();
|
||||
(function () {
|
||||
if (window.__palette) return; window.__palette = true;
|
||||
var index = null, items = [], sel = 0;
|
||||
|
||||
+13
-3
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user