144 lines
8.6 KiB
HTML
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'), 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 %}
|