Files
Onyscribe-Admin/app/templates/core.html
RubenRWU 7d8c226da2 Onyscribe Admin: Initialer Stand
Admin-Plattform für die Tenants von Onyscribe: Editor mit Live-Vorschau (echter Core in Sandbox), Versionen, KI-Agent (Cortecs),
OIDC-Abgleich über die Login-Developer-API, Deploy-Engine (Mittwald, gesperrt ohne DEPLOY_LIVE, Freigabe pro Tenant),
Core-Vertrag (Schema/Manifest), Core-Releases mit Kompatibilitätsprüfung, Canary-Rollout und Rollback, Zustandsabgleich.
Runbook: docs/RUNBOOK_PARALLELTEST.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 15:30:26 +02:00

91 lines
9.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Core · Onyscribe Admin{% endblock %}
{% block body %}
<div class="page">
<div class="head"><div><div class="eyebrow">Zusammenspiel mit dem Core</div><h1 class="title">Core-Releases.</h1>
<p class="lead">Die Core-CI meldet neue Images hierher. Die Admin-Plattform prüft damit <b>alle</b> Tenants, gibt nach deiner Freigabe aus und rollt Canary-zuerst aus – mit Health-Check pro Tenant und Rollback.</p></div>
<button class="btn" onclick="load()">Aktualisieren</button></div>
<div id="warn"></div>
<div class="grid2">
<div class="card"><h3>Core der Admin-Plattform</h3><dl class="kv" id="admin-core"></dl>
<p class="hint" style="margin-top:8px">Vorschau, Sandbox und Formulare nutzen genau diesen Core. Im Container ist das Admin-Image <span class="mono">FROM</span> das Core-Image gebaut – Schriften, Python-Pakete und WeasyPrint sind damit identisch zur Produktion.</p></div>
<div class="card"><h3>Rollout-Regeln</h3><dl class="kv" id="rules"></dl>
<div class="row" style="margin-top:8px"><label class="f" style="flex:1">Canary-Tenants (zuerst, kommagetrennt)<input type="text" id="canary" placeholder="demo"></label><button class="btn sm" onclick="saveCanary()" style="align-self:flex-end">Speichern</button></div>
<p class="hint" style="margin-top:6px">Gepinnte Tenants (Tenant → Übersicht → Core-Version) nehmen nicht teil. Der Rollout bricht beim ersten Fehler ab; Echt-Rollouts brauchen <span class="mono">DEPLOY_LIVE=true</span>.</p></div>
</div>
<div class="card" style="margin-top:12px"><h3>Releases</h3><div id="rel"></div></div>
</div>
<div class="modal-bg" id="m-roll"><div class="modal"><h2 id="roll-title">Rollout</h2><p class="hint" id="roll-hint"></p><table class="t" id="roll-table"><tbody></tbody></table>
<div class="acts"><button class="btn" onclick="modal('m-roll',false);load()">Schließen</button></div></div></div>
{% endblock %}
{% block scripts %}
{% raw %}<script>
let D = null, open_ = {}, timer = null;
const stLabel = {new:'neu', checked:'geprüft', approved:'freigegeben', rolling:'wird ausgerollt', active:'aktiv', failed:'fehlgeschlagen', rejected:'abgelehnt', superseded:'abgelöst', rolled_back:'zurückgerollt'};
const stCls = {active:'ok', approved:'cyan', checked:'', new:'warn', rolling:'cyan', failed:'err', rejected:'err', superseded:'', rolled_back:'warn'};
async function load(){
D = await api('GET','/api/core/releases');
const a = D.admin_core;
document.getElementById('admin-core').innerHTML = `<dt>Core-SHA</dt><dd class="mono">${esc(a.sha.slice(0,12))} <span class="muted">(${esc(a.source)})</span></dd><dt>Gebaut</dt><dd>${a.built_at?fmt(a.built_at):'–'}</dd>
<dt>Vertrag</dt><dd>v${a.contract} ${a.supported?'<span class="badge ok">unterstützt</span>':'<span class="badge err">nicht unterstützt</span>'}</dd><dt>Schema</dt><dd>${a.fields??'–'} Felder · ${a.env??'–'} Env-Einträge <span class="muted mono">${esc((a.schema_sha||'').slice(0,8))}</span></dd>`;
document.getElementById('rules').innerHTML = `<dt>Aktiver Tag</dt><dd class="mono">:${esc(String(D.active_tag).slice(0,12))}</dd><dt>Release-Eingang</dt><dd>${D.intake?'<span class="badge ok">Token gesetzt</span>':'<span class="badge warn">CORE_RELEASE_TOKEN fehlt</span>'} <span class="hint">CI: Job „notify-admin“</span></dd><dt>Rollout</dt><dd>${D.live?'<span class="badge err">live erlaubt</span>':'<span class="badge warn">gesperrt · nur Dry-Run</span>'}</dd>`;
document.getElementById('canary').value = D.canary.join(', ');
document.getElementById('warn').innerHTML = a.supported ? '' : '<div class="banner err">Dieser Core nutzt einen Vertrag, den diese Admin-Version nicht kennt – Admin aktualisieren, bevor etwas ausgerollt wird.</div>';
renderRel();
if(D.releases.some(r=>r.compat&&r.compat.running||r.status==='rolling')){ clearTimeout(timer); timer = setTimeout(load, 2500); }
}
function matrix(r){
const c = r.compat; if(!c) return '<div class="hint">Noch nicht geprüft.</div>';
const rows = Object.entries(c.tenants||{}).map(([t,v])=>`<tr><td><b>${esc(t)}</b></td><td><span class="badge ${v.status==='ok'?'ok':v.status==='error'?'err':v.status==='changed'?'warn':''}">${{ok:'unverändert',changed:'Rendering geändert',new:'neu (keine Baseline)',error:'Fehler'}[v.status]}</span></td>
<td class="muted">${v.changed.length?esc(v.changed.join(', ')):''}${v.errors.map(e=>`<div class="mono" style="color:var(--err)">${esc(e).slice(0,300)}</div>`).join('')}${v.warnings.map(w=>`<div>⚠ ${esc(w)}</div>`).join('')}</td></tr>`).join('');
return `<div class="hint" style="margin:4px 0">${c.running?'Prüfung läuft …':'Geprüft '+fmt(c.checked_at)+' mit Core '+esc((c.core_sha||'').slice(0,10))}${c.forced?' · <b>Abweichender Admin-Core (erzwungen)</b>':''} · ${Object.entries(c.summary||{}).filter(([k,v])=>v).map(([k,v])=>v+'× '+k).join(', ')}</div>
<table class="t"><tbody>${rows}</tbody></table>`;
}
function renderRel(){
document.getElementById('rel').innerHTML = D.releases.length ? `<table class="t"><thead><tr><th>Release</th><th>Nachricht</th><th>Gemeldet</th><th>Status</th><th>Admin</th><th></th></tr></thead><tbody>`+D.releases.map(r=>{
const c = r.compat, sum = c&&c.summary ? c.summary : null;
return `<tr><td class="nowrap"><span class="mono">${esc(r.sha.slice(0,10))}</span> ${r.active?'<span class="badge ok">aktiv</span>':''}<div class="hint">${esc(r.ref)}</div></td><td>${esc(r.message)}</td><td class="nowrap">${fmt(r.announced_at)}</td>
<td><span class="badge ${stCls[r.status]||''}">${stLabel[r.status]||r.status}</span>${sum?`<div class="hint">${sum.ok||0} ok · ${sum.changed||0} geändert · ${sum.new||0} neu · ${sum.error||0} Fehler</div>`:''}</td>
<td>${r.admin_matches?'<span class="badge ok">gleicher Core</span>':'<span class="badge warn" title="Admin muss FROM diesem Core neu gebaut werden">Admin älter/anders</span>'}</td>
<td style="text-align:right" class="nowrap"><button class="btn sm" onclick="tg(${r.id})">Matrix</button>
<button class="btn sm" onclick="act(${r.id},'check')">Prüfen</button>
<button class="btn sm" ${['checked','approved','active','failed','rolled_back'].includes(r.status)?'':'disabled'} onclick="approve(${r.id})">Freigeben</button>
<button class="btn sm" onclick="rollout(${r.id},true)">Dry-Run</button>
<button class="btn sm cyan ${D.live&&['approved','active','failed','rolled_back'].includes(r.status)?'':'disabled'}" onclick="rollout(${r.id},false)">Ausrollen</button>
${r.previous_tag?`<button class="btn sm danger ${D.live?'':'disabled'}" onclick="rollback(${r.id})">Zurückrollen</button>`:''}
${['new','checked'].includes(r.status)?`<button class="btn sm danger" onclick="act(${r.id},'reject')">Ablehnen</button>`:''}</td></tr>
<tr style="display:${open_[r.id]?'table-row':'none'}"><td colspan="6">${matrix(r)}</td></tr>`}).join('')+'</tbody></table>'
: '<div class="empty">Noch kein Release gemeldet. Die Core-CI meldet Images per POST /api/core/releases (Job „notify-admin“).</div>';
}
function tg(id){ open_[id] = !open_[id]; renderRel(); }
async function act(id, what){
try{ await api('POST', `/api/core/releases/${id}/${what}`, what==='check' ? {force: !D.releases.find(r=>r.id===id).admin_matches && confirm('Die Admin-Plattform läuft nicht auf diesem Core. Trotzdem mit dem aktuellen Admin-Core prüfen (nur Entwicklung)?')} : {}); toast(what==='check'?'Prüfung gestartet':'ok'); open_[id]=true; load(); }
catch(e){ toast(e.message,1) }
}
async function approve(id){
const r = D.releases.find(x=>x.id===id); const changed = r.compat&&r.compat.summary&&r.compat.summary.changed;
if(changed && !confirm(changed+' Tenant(s) rendern anders als zuvor (siehe Matrix). Bewusst freigeben?')) return;
try{ await api('POST', `/api/core/releases/${id}/approve`, {acknowledge: !!changed}); toast('Freigegeben'); load(); }catch(e){ toast(e.message,1) }
}
async function rollout(id, dry){
if(!dry && !confirm('Release jetzt Canary-zuerst auf alle (nicht gepinnten) Tenants ausrollen? Bei einem Fehler wird abgebrochen.')) return;
let r; try{ r = await api('POST', `/api/core/releases/${id}/rollout`, {dry_run: dry}); }catch(e){ return toast(e.message,1) }
followBatch(r.batch, dry ? 'Dry-Run des Rollouts' : 'Rollout läuft');
}
async function rollback(id){
if(!confirm('Alle Tenants auf den Tag vor diesem Rollout zurückrollen?')) return;
try{ const r = await api('POST', `/api/core/releases/${id}/rollback`); followBatch(r.batch, 'Rollback läuft'); }catch(e){ toast(e.message,1) }
}
function followBatch(batch, title){
document.getElementById('roll-title').textContent = title; document.getElementById('roll-hint').textContent = 'Reihenfolge: Canary zuerst, dann der Rest; Abbruch beim ersten Fehler.'; modal('m-roll', true);
const tick = async () => {
const rows = await api('GET', '/api/deployments?batch=' + batch);
document.querySelector('#roll-table tbody').innerHTML = rows.map(d=>{ const last=(d.log.trim().split('\n').filter(l=>!l.includes('[DRY-RUN]')&&!l.includes('body:')).pop()||'').slice(10); const cls=d.status==='success'?'ok':d.status==='failed'?'err':d.status==='running'?'cyan':''; return `<tr><td><b>${esc(d.tenant)}</b></td><td class="mono">:${esc(String(d.image).slice(0,10))}</td><td><span class="badge ${cls}">${d.status}</span></td><td class="muted mono">${esc(last)}</td></tr>`; }).join('');
if(rows.some(d=>d.status==='running'||d.status==='queued')) setTimeout(tick, 800);
}; tick();
}
async function saveCanary(){ try{ await api('PUT','/api/settings',{canary_tenants: document.getElementById('canary').value}); toast('Gespeichert'); }catch(e){ toast(e.message,1) } }
load();
</script>{% endraw %}
{% endblock %}