Files
release-agent/admin.html
jbergner 4a9a4ad65d
All checks were successful
build-binaries / build (, amd64, linux) (push) Has been skipped
build-binaries / build (, arm, 7, linux) (push) Has been skipped
build-binaries / build (, arm64, linux) (push) Has been skipped
build-binaries / build (.exe, amd64, windows) (push) Has been skipped
build-binaries / release (push) Has been skipped
build-binaries / publish-agent (push) Has been skipped
release-tag / release-image (push) Successful in 2m7s
Updated for Multi-Product
2025-10-26 10:45:18 +01:00

270 lines
12 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Version Agent Admin</title>
<style>
:root { --gap: 12px; }
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell;max-width:1100px;margin:40px auto;padding:0 16px}
header{display:flex;gap:24px;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;margin-bottom:24px}
section{border:1px solid #ddd;border-radius:12px;padding:16px;margin-bottom:24px;box-shadow:0 1px 3px rgba(0,0,0,.05)}
h1{font-size:1.4rem;margin:0 0 8px}
h2{font-size:1.1rem;margin:.2rem 0 .8rem}
label{display:block;margin:.3rem 0 .1rem;color:#333}
input,select,textarea{width:100%;padding:.5rem;border:1px solid #ccc;border-radius:8px}
button{padding:.6rem 1rem;border:0;border-radius:10px;cursor:pointer}
.btn{background:#111;color:#fff}
.muted{color:#666}
.row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap)}
.row-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap)}
.row-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap)}
.assets{margin-top:8px}
.asset-row{display:grid;grid-template-columns:2fr 2fr 1fr 2fr;gap:8px;margin-bottom:8px}
pre{background:#f6f6f6;padding:8px;border-radius:8px;overflow:auto;max-height:360px}
.right{display:flex;gap:8px;align-items:end}
</style>
</head>
<body>
<header>
<div>
<h1>Version Agent Admin</h1>
<div class="muted">Pflege mehrerer Produkte pro Vendor</div>
</div>
<div class="right">
<div>
<label>API Token (für POST)</label>
<input id="token" placeholder="Bearer Token" />
<div class="muted">lokal gespeichert</div>
</div>
<div>
<label>Produkt</label>
<div class="row-2">
<select id="product"></select>
<button id="makeDefault" title="Als Default-Produkt setzen">Als Default</button>
</div>
<div class="muted">wird in <code>localStorage</code> gemerkt</div>
</div>
</div>
</header>
<section>
<h2>Produkt-Konfiguration</h2>
<div class="row">
<div>
<label>Default Branch</label>
<input id="defBranch" placeholder="z.B. 12.x" />
</div>
<div>
<label>Default Channel</label>
<select id="defChannel"></select>
</div>
<div>
<label>Vendor (optional global)</label>
<input id="vendor" placeholder="Vendor-Name" />
</div>
<div style="display:flex;gap:8px;align-items:flex-end">
<button class="btn" id="saveProductCfg">Produkt-Defaults speichern</button>
<button id="saveVendorCfg">Vendor speichern</button>
</div>
</div>
</section>
<section>
<h2>Pflege: Latest setzen</h2>
<div class="row">
<div><label>Branch</label><input id="branch" placeholder="z.B. 12.x"/></div>
<div><label>Channel</label><select id="channel"></select></div>
<div><label>Arch</label><select id="arch"></select></div>
<div><label>Bit</label><select id="bit"></select></div>
</div>
<div class="row" style="margin-top:var(--gap)">
<div><label>OS</label><select id="os"></select></div>
<div><label>Version</label><input id="version" placeholder="12.3.1"/></div>
<div><label>Build</label><input id="build" placeholder="optional"/></div>
<div><label>Released At (RFC3339)</label><input id="releasedAt" placeholder="2025-10-15T12:34:56Z"/></div>
</div>
<div style="margin-top:var(--gap)">
<label>Notes URL</label><input id="notesUrl" placeholder="https://example.com/release-notes"/>
</div>
<div class="assets">
<h3>Assets</h3>
<div id="assets"></div>
<button id="addAsset">Asset hinzufügen</button>
</div>
<div style="margin-top:var(--gap)"><button class="btn" id="publish">Publish</button> <button id="loadLatest">Aktuelles Latest laden</button></div>
<pre id="log"></pre>
</section>
<section>
<h2>Manifest (Produkt-spezifisch)</h2>
<div class="muted">GET <code>/v1/manifest?product=...</code></div>
<pre id="manifest"></pre>
</section>
<section>
<h2>Neues Produkt anlegen</h2>
<div class="row-3">
<div><label>Produkt-Name</label><input id="newProdName" placeholder="z.B. wol-server"/></div>
<div><label>Default Branch</label><input id="newProdBranch" placeholder="z.B. 1.x"/></div>
<div><label>Default Channel</label><select id="newProdChannel"></select></div>
</div>
<div style="margin-top:var(--gap)"><button id="createProduct">Produkt anlegen/aktualisieren</button></div>
</section>
<script>
const $ = s => document.querySelector(s);
const tokenHeader = () => { const t=$('#token').value.trim(); return t? { 'Authorization': 'Bearer '+t } : {}; }
const api = (path, opt={}) => fetch(path, opt);
const product = () => $('#product').value.trim();
const log = (msg) => { const el=$('#log'); el.textContent = (new Date()).toISOString()+"\n"+msg; }
function fillSelect(sel, arr, val){ const s=$(sel); s.innerHTML=''; arr.forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; s.appendChild(o); }); if(val && arr.includes(val)) s.value=val; }
function addAssetRow(data={}){
const wrap=document.createElement('div'); wrap.className='asset-row';
wrap.innerHTML = `
<input placeholder="URL" value="${data.url||''}"/>
<input placeholder="SHA256" value="${data.sha256||''}"/>
<input placeholder="Size (bytes)" value="${data.size_bytes||''}"/>
<input placeholder="Signature URL" value="${data.signature_url||''}"/>
`;
$('#assets').appendChild(wrap);
}
async function loadProducts(){
const r = await api('/v1/products');
const j = await r.json();
const list = j.products || [];
const def = j.default || '';
const last = localStorage.getItem('product') || '';
const chosen = list.includes(last) ? last : (def || list[0] || '');
fillSelect('#product', list, chosen);
localStorage.setItem('product', chosen||'');
await Promise.all([loadValues(), loadManifest()]);
}
async function loadValues(){
const r = await api('/v1/values?product='+encodeURIComponent(product()));
const j = await r.json();
fillSelect('#arch', j.arch||[]);
fillSelect('#bit', j.bit||[]);
fillSelect('#os', j.os||[]);
fillSelect('#channel', j.channels||[], (j.defaults && j.defaults.channel) || 'stable');
fillSelect('#defChannel', j.channels||[], (j.defaults && j.defaults.channel) || 'stable');
fillSelect('#newProdChannel', j.channels||[], 'stable');
$('#defBranch').value = (j.defaults && j.defaults.branch) || '';
$('#vendor').value = (j.meta && j.meta.vendor) || '';
// Vorauswahl für Publish-Form
$('#branch').value = $('#defBranch').value;
$('#assets').innerHTML=''; addAssetRow();
}
async function loadManifest(){
const p = product();
const r = await api('/v1/manifest'+(p? ('?product='+encodeURIComponent(p)) : ''));
const j = await r.json();
$('#manifest').textContent = JSON.stringify(j, null, 2);
}
async function saveProductCfg(){
const p = product(); if(!p){ alert('Kein Produkt gewählt'); return }
const payload = { DefaultBranch: $('#defBranch').value.trim(), DefaultChannel: $('#defChannel').value };
const r = await api('/v1/config?product='+encodeURIComponent(p), { method:'POST', headers:{ 'Content-Type':'application/json', ...tokenHeader() }, body: JSON.stringify(payload) });
const txt = await r.text(); log(txt); await Promise.all([loadValues(), loadManifest()]);
}
async function saveVendorCfg(){
const payload = { Vendor: $('#vendor').value.trim() };
const r = await api('/v1/config', { method:'POST', headers:{ 'Content-Type':'application/json', ...tokenHeader() }, body: JSON.stringify(payload) });
const txt = await r.text(); log(txt);
}
async function makeDefault(){
const p = product(); if(!p){ alert('Kein Produkt gewählt'); return }
const payload = { DefaultProduct: p };
const r = await api('/v1/config', { method:'POST', headers:{ 'Content-Type':'application/json', ...tokenHeader() }, body: JSON.stringify(payload) });
const txt = await r.text(); log(txt); await loadProducts();
}
async function createProduct(){
const name = $('#newProdName').value.trim(); if(!name){ alert('Produkt-Name fehlt'); return }
const payload = { product: name, default_branch: $('#newProdBranch').value.trim(), default_channel: $('#newProdChannel').value };
const r = await api('/v1/products', { method:'POST', headers:{ 'Content-Type':'application/json', ...tokenHeader() }, body: JSON.stringify(payload) });
const txt = await r.text(); log(txt);
await loadProducts();
$('#product').value = name; localStorage.setItem('product', name);
await Promise.all([loadValues(), loadManifest()]);
}
async function publish(){
const p = product(); if(!p){ alert('Kein Produkt gewählt'); return }
const assets = Array.from(document.querySelectorAll('.asset-row')).map(row=>{
const [url,sha,size,sig] = row.querySelectorAll('input');
const a={ url:url.value.trim(), sha256:sha.value.trim() };
if(size.value.trim()) a.size_bytes = parseInt(size.value.trim(),10);
if(sig.value.trim()) a.signature_url = sig.value.trim();
return a;
}).filter(a=>a.url && a.sha256);
const payload = {
branch: $('#branch').value.trim() || $('#defBranch').value.trim(),
channel: $('#channel').value,
arch: $('#arch').value,
bit: $('#bit').value,
os: $('#os').value,
release: {
version: $('#version').value.trim(),
build: $('#build').value.trim(),
released_at: $('#releasedAt').value.trim(),
notes_url: $('#notesUrl').value.trim(),
assets
}
};
const r = await api('/v1/publish?product='+encodeURIComponent(p), { method:'POST', headers:{ 'Content-Type':'application/json', ...tokenHeader() }, body: JSON.stringify(payload) });
const txt = await r.text(); log(txt); await loadManifest();
}
async function loadLatest(){
const p = product(); if(!p){ alert('Kein Produkt gewählt'); return }
const params = new URLSearchParams({
product: p,
branch: $('#branch').value.trim() || $('#defBranch').value.trim(),
channel: $('#channel').value,
arch: $('#arch').value,
bit: $('#bit').value,
os: $('#os').value
});
const r = await api('/v1/latest?'+params.toString());
if(!r.ok){ log('not found'); return; }
const j = await r.json();
$('#version').value = j.release.version || '';
$('#build').value = j.release.build || '';
$('#releasedAt').value = (j.release.released_at||'');
$('#notesUrl').value = j.release.notes_url || '';
$('#assets').innerHTML='';
(j.release.assets||[]).forEach(a=>addAssetRow({ url:a.url, sha256:a.sha256, size_bytes:a.size_bytes||'', signature_url:a.signature_url||'' }));
if((j.release.assets||[]).length===0) addAssetRow();
}
// init
(function(){
$('#token').value = localStorage.getItem('apiToken')||'';
$('#token').addEventListener('input', e=> localStorage.setItem('apiToken', e.target.value));
$('#product').addEventListener('change', async e=>{ localStorage.setItem('product', e.target.value); await Promise.all([loadValues(), loadManifest()]); });
$('#addAsset').addEventListener('click', e=>{ e.preventDefault(); addAssetRow(); });
$('#publish').addEventListener('click', e=>{ e.preventDefault(); publish(); });
$('#loadLatest').addEventListener('click', e=>{ e.preventDefault(); loadLatest(); });
$('#saveProductCfg').addEventListener('click', e=>{ e.preventDefault(); saveProductCfg(); });
$('#saveVendorCfg').addEventListener('click', e=>{ e.preventDefault(); saveVendorCfg(); });
$('#makeDefault').addEventListener('click', e=>{ e.preventDefault(); makeDefault(); });
$('#createProduct').addEventListener('click', e=>{ e.preventDefault(); createProduct(); });
addAssetRow(); loadProducts();
})();
</script>
</body>
</html>