Files
root 6e5bb5eac1
CD / deploy (push) Successful in 24s
CI / check (push) Successful in 32s
feat(ops): værtens apt-sikkerhedsopdateringer på driftssiden
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

144 lines
8.6 KiB
HTML

{% extends "base.html" %}
{% block title %}Admin — drift{% endblock %}
{% block body %}
<h1>Drift / egress</h1>
<p class="lead">Live driftsoverblik — versioner, opdateringer og random-proxy.
Opdaterer automatisk hvert 10. sekund i baggrunden.</p>
<div class="opstools">
<div class="filter" style="margin:0;flex:1;min-width:11rem">
<span class="mag">⌕</span>
<input id="opsfilter" type="search" placeholder="Søg tjeneste…" autocomplete="off" aria-label="Søg tjeneste">
</div>
<div class="segmented" id="opsmode" role="tablist" aria-label="Filtrér">
<button type="button" data-mode="all" class="on">Alle</button>
<button type="button" data-mode="update">Opdateringer</button>
<button type="button" data-mode="eol">EOL</button>
</div>
<div class="segmented" id="opslevel" role="tablist" aria-label="Niveau">
<button type="button" data-level="all" class="on">Alle niveauer</button>
<button type="button" data-level="major">Major</button>
<button type="button" data-level="minor">Minor</button>
<button type="button" data-level="patch">Bugfix</button>
</div>
</div>
<div id="opscontent">
<div class="grid" aria-hidden="true" style="margin-top:1.4rem">
{% for i in range(6) %}
<div class="stat" style="opacity:.5"><span class="sk" style="width:60%;height:1.6em;display:block;border-radius:6px"></span>
<span class="sk" style="width:40%;height:.8em;display:block;margin-top:.6rem;border-radius:5px"></span></div>
{% endfor %}
</div>
</div>
<style>
.sk { background:var(--surface2); }
.font-mono { font-family: ui-monospace, "Cascadia Code", Menlo, Consolas, monospace; }
.vtable td { vertical-align:middle; }
.vbadge { display:inline-block; font-size:.74rem; font-weight:600; border-radius:999px; padding:.1rem .6rem; border:1px solid var(--border); color:var(--muted); white-space:nowrap; }
.vbadge.ok { color:#3ecf8e; border-color:color-mix(in srgb,#3ecf8e 40%,transparent); background:color-mix(in srgb,#3ecf8e 10%,transparent); }
.vbadge.update { color:#f5a623; border-color:color-mix(in srgb,#f5a623 50%,transparent); background:color-mix(in srgb,#f5a623 12%,transparent); }
.vbadge.muted { color:var(--muted); }
.vbadge.soon { color:#f5a623; border-color:color-mix(in srgb,#f5a623 50%,transparent); background:color-mix(in srgb,#f5a623 12%,transparent); }
.vbadge.eol { color:#f87171; border-color:color-mix(in srgb,#f87171 55%,transparent); background:color-mix(in srgb,#f87171 14%,transparent); font-weight:700; }
/* opdaterings-niveau-farver */
.vbadge.lvl-major { color:#fb923c; border-color:color-mix(in srgb,#fb923c 50%,transparent); background:color-mix(in srgb,#fb923c 12%,transparent); }
.vbadge.lvl-patch { color:var(--muted); border-color:var(--border); background:transparent; }
/* EOL-celle på ÉN linje: kompakt badge + lille næste-major */
.vtable td:last-child { white-space:nowrap; }
.nextmajor { margin-left:.35rem; font-size:.72rem; color:var(--accent); white-space:nowrap; }
.opslead { display:flex; align-items:baseline; gap:.7rem; flex-wrap:wrap; }
/* filter-værktøjslinje (persistent i shell, overlever 10s-poll) */
.opstools { display:flex; gap:.8rem; align-items:center; flex-wrap:wrap; margin:1.2rem 0 .2rem; }
.segmented { display:inline-flex; border:1px solid var(--border); border-radius:.7rem; overflow:hidden; }
.segmented button { background:transparent; color:var(--muted); border:0; padding:.5rem .9rem; cursor:pointer; font:inherit; font-size:.85rem; }
.segmented button.on { background:var(--surface2); color:var(--text); }
/* opdaterings-/EOL-markering på rækker (desktop: tonet baggrund) */
.updrow td, .eolnear td { background:color-mix(in srgb,#f5a623 7%,transparent); }
.eolsoon td { background:color-mix(in srgb,#f5a623 14%,transparent); }
.eolrow td { background:color-mix(in srgb,#f87171 9%,transparent); }
.slot { background:color-mix(in srgb, var(--surface) 70%, transparent); border:1px solid var(--border); border-radius:.9rem; padding:.8rem .9rem; }
.slot.next { border-color:#f5a623; box-shadow:0 0 0 1px color-mix(in srgb,#f5a623 40%,transparent); }
.slot-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.35rem; }
.slot-id { font-weight:700; color:var(--muted); font-size:.85rem; }
.slot-state { font-size:.72rem; border-radius:999px; padding:.05rem .5rem; border:1px solid var(--border); color:var(--muted); }
.slot-state.live { color:#3ecf8e; border-color:color-mix(in srgb,#3ecf8e 40%,transparent); }
.slot-state.busy { color:#f5a623; border-color:color-mix(in srgb,#f5a623 40%,transparent); }
.slot-state.down { color:#f87171; border-color:color-mix(in srgb,#f87171 40%,transparent); }
.slot-next { color:#f5a623; font-size:.74rem; margin-top:.35rem; }
#opscontent.stale { opacity:.55; transition:opacity .3s; }
/* mobil: versionstabellen kollapser til kort (ét pr. tjeneste) */
@media (max-width: 640px) {
.vtable thead { display:none; }
.vtable, .vtable tbody, .vtable tr, .vtable td { display:block; width:100%; }
.vtable tr { border:1px solid var(--border); border-radius:.9rem; padding:.55rem .85rem;
margin-bottom:.7rem; background:color-mix(in srgb,var(--surface) 70%,transparent); }
.vtable tr.updrow, .vtable tr.eolnear { border-color:color-mix(in srgb,#f5a623 50%,transparent); }
/* EOL < 3 mdr: hele kortet gult */
.vtable tr.eolsoon { border-color:color-mix(in srgb,#f5a623 70%,transparent);
background:color-mix(in srgb,#f5a623 22%,transparent); }
.vtable tr.eolrow { border-color:color-mix(in srgb,#f87171 60%,transparent);
background:color-mix(in srgb,#f87171 18%,transparent); }
.vtable tr.eolsoon td:first-child, .vtable tr.eolrow td:first-child { border-bottom-color:color-mix(in srgb,var(--text) 18%,transparent); }
.vtable td { border:0; background:transparent; padding:.2rem 0; display:flex;
justify-content:space-between; gap:1rem; align-items:center; text-align:right; }
.vtable td:first-child { font-size:1.05rem; padding:.1rem 0 .45rem; margin-bottom:.35rem;
border-bottom:1px solid var(--border); }
.vtable td:not(:first-child)::before { content:attr(data-label); color:var(--muted);
font-size:.76rem; font-weight:600; text-align:left; letter-spacing:.02em; }
.vtable td .vbadge { white-space:normal; text-align:right; }
.vtable td:last-child { white-space:normal; }
}
</style>
<script>
(function(){
var box = document.getElementById('opscontent'), busy = false;
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[data-name]'), shown = 0;
rows.forEach(function(tr){
var name = tr.getAttribute('data-name') || '',
st = tr.getAttribute('data-status'), lvl = tr.getAttribute('data-level'),
eol = tr.getAttribute('data-eol');
var okq = !fstate.q || name.indexOf(fstate.q) > -1;
var isEol = (eol === 'eol' || eol === 'soon');
var okm = fstate.mode === 'all'
|| (fstate.mode === 'update' && (st === 'update' || isEol))
|| (fstate.mode === 'eol' && isEol);
// niveau-filter rammer kun opdateringer (kun de har data-level)
var okl = fstate.level === 'all' || lvl === fstate.level;
var vis = okq && okm && okl; tr.style.display = vis ? '' : 'none'; if (vis) shown++;
});
var nm = box.querySelector('#opsnomatch');
if (nm) nm.style.display = (rows.length && !shown) ? 'block' : 'none';
}
var fi = document.getElementById('opsfilter');
if (fi) fi.addEventListener('input', function(){ fstate.q = fi.value.trim().toLowerCase(); applyFilter(); });
function wireSeg(id, key){
var bar = document.getElementById(id); if (!bar) return;
bar.addEventListener('click', function(e){
var b = e.target.closest('button[data-' + key + ']'); if (!b) return;
fstate[key] = b.getAttribute('data-' + key);
bar.querySelectorAll('button').forEach(function(x){ x.classList.toggle('on', x === b); });
applyFilter();
});
}
wireSeg('opsmode', 'mode');
wireSeg('opslevel', 'level');
function load(){
if (busy) return; busy = true;
fetch('/ops-content', {headers:{'X-Requested-With':'fetch'}})
.then(function(r){ if(!r.ok) throw 0; return r.text(); })
.then(function(html){ box.innerHTML = html; box.classList.remove('stale'); applyFilter(); })
.catch(function(){ box.classList.add('stale'); })
.finally(function(){ busy = false; });
}
load();
setInterval(load, 10000);
document.addEventListener('visibilitychange', function(){ if(!document.hidden) load(); });
})();
</script>
{% endblock %}