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
270 lines
12 KiB
HTML
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>
|