reconcile: import current prod state from CT151 (media-cleanup, access admin, ops versioning, newer dashboard)
This commit is contained in:
+133
-42
@@ -2,51 +2,142 @@
|
||||
{% block title %}Admin — drift{% endblock %}
|
||||
{% block body %}
|
||||
<h1>Drift / egress</h1>
|
||||
<p class="wt-lead">Live driftsoverblik. Genindlæser automatisk hvert 20. sekund.</p>
|
||||
<p class="lead">Live driftsoverblik — versioner, opdateringer og random-proxy.
|
||||
Opdaterer automatisk hvert 10. sekund i baggrunden.</p>
|
||||
|
||||
<h2>Static-assets</h2>
|
||||
<div class="wt-stat" style="max-width:18rem">
|
||||
<div class="wt-stat-n">{{ version if version else '—' }}</div>
|
||||
<div class="wt-muted">deployet version (assets.weircon.dk)</div>
|
||||
</div>
|
||||
|
||||
<h2 style="display:flex;align-items:baseline;gap:.8rem">Random-proxy egress
|
||||
{% if rp and not rp.error %}<span class="wt-muted" style="font-size:.8rem;letter-spacing:0;text-transform:none">
|
||||
{{ rp.available }}/{{ rp.proxies_total }} live · {{ rp.inflight_total }} inflight i alt</span>{% endif %}
|
||||
</h2>
|
||||
{% if rp.error %}
|
||||
<div class="wt-flash wt-flash--error">Kunne ikke nå random-proxy: {{ rp.error }}</div>
|
||||
{% else %}
|
||||
{% set rot = rp.rotation %}
|
||||
{% if rot %}
|
||||
<div class="wt-stat" style="margin-bottom:1rem">
|
||||
<div class="wt-muted">Næste rollover</div>
|
||||
<div style="margin-top:.3rem">
|
||||
<strong>slot {{ rot.next_slot }}</strong>
|
||||
— <span class="wt-mono">{{ rot.next_egress_ip }}</span>
|
||||
{% if rot.geo and rot.geo.flag %}<span title="{{ rot.geo.name }}">{{ rot.geo.flag }} {{ rot.geo.iso }}</span>{% endif %}
|
||||
ruller af om <strong>{{ '%d:%02d'|format(rot.eta_sec//60, rot.eta_sec%60) }}</strong>
|
||||
<span class="wt-muted">(interval {{ rot.interval_sec }}s · sidst: slot {{ rot.last_slot }} for {{ rot.last_rotated_age_sec }}s siden)</span>
|
||||
<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>
|
||||
{% endif %}
|
||||
<div class="wt-grid" style="grid-template-columns:repeat(auto-fill,minmax(190px,1fr))">
|
||||
{% for p in rp.proxies %}
|
||||
<div class="slot{% if rot and p.id == rot.next_slot %} next{% endif %}">
|
||||
<div class="slot-top">
|
||||
<span class="slot-id">#{{ p.id }}</span>
|
||||
<span class="slot-state {{ 'down' if not p.reachable else ('busy' if p.inflight else 'live') }}">
|
||||
{{ '⚠ down' if not p.reachable else (p.state or 'live') }}</span>
|
||||
</div>
|
||||
<div class="wt-mono" style="font-size:.92rem;overflow:hidden;text-overflow:ellipsis">{{ p.egress_ip or '–' }}</div>
|
||||
<div class="wt-muted" style="font-size:.84rem;margin-top:.2rem">
|
||||
{% if p.geo and p.geo.flag %}{{ p.geo.flag }} {{ p.geo.iso }} · {% endif %}{{ p.inflight or 0 }} inflight
|
||||
{% if p.latency_ms %}· {{ p.latency_ms }}ms{% endif %}
|
||||
</div>
|
||||
{% if rot and p.id == rot.next_slot %}<div class="slot-next">↻ ruller af næst</div>{% endif %}
|
||||
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<script>setTimeout(function(){ location.reload(); }, 20000);</script>
|
||||
|
||||
<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 %}
|
||||
|
||||
Reference in New Issue
Block a user