`);
}
function runRows(runs) {
return runs.map(r=>{
const steps=arr(r.steps),finished=steps.filter(s=>['succeeded','failed','skipped'].includes(s.status)).length;
const waiting={prepared:'Freigabe aktiv · wartet auf ISO',answer_served:'Antwort ausgeliefert',answer_delivered:'Antwort ausgeliefert',installed_reported:'Wartet auf den ersten Start',installing:'Basisinstallation',installed:'Wartet auf den Runner'}[r.status];
const progress=steps.length?Math.round(finished/steps.length*100):0;
const progressCell=waiting?esc(waiting):steps.length?`${finished} / ${steps.length} geprüft`:esc(fmtDate(r.started_at || r.created_at));
return `
${runs.length?table(['SERVER','STATUS','FORTSCHRITT',''],runRows(runs.slice(0,6))):empty('Bereit für den ersten Lauf','Erfassen Sie einen Server, weisen Sie geprüfte Profile zu und erteilen Sie die Installationsfreigabe.','activity',canOperate()?actionButton('Server erfassen','create-host','plus'):'')}
${hosts.length?table(['SERVER','STANDORT','STATUS',''],hostRows(hosts.slice(0,5),true)):empty('Ihr Inventar beginnt hier','Identitäten, Managementnetz und Profilzuordnungen an einem Ort.','server')}
`;
}
function hostsPage(hosts, discoveries=[]) {
const unassigned=discoveries.filter(d=>!hosts.some(h=>arr(h.identities).some(i=>arr(d.identities).some(v=>v.kind===i.kind && v.value.toLowerCase()===i.value.toLowerCase()))));
return header('Serverinventar','Serveridentitäten, Netzwerke und freigegebene Konfigurationen verwalten.',actionButton('Aktualisieren','refresh','refresh')+(canOperate()?actionButton('Mehrere Server erfassen','import-hosts','file')+actionButton('Server hinzufügen','create-host','plus','',true):''),'VERWALTUNG / SERVER')+`${toolbar('FQDN, IP-Adresse oder Tag suchen …',[...new Set(hosts.map(h=>h.site).filter(Boolean))],[...new Set(hosts.map(h=>h.blocked?'blocked':h.status).filter(Boolean))])}${hosts.length?table(['SERVER','STANDORT','STATUS','TAGS','LETZTER KONTAKT',''],hostRows(hosts)):empty('Noch keine Server erfasst','Erfassen Sie einen Host anhand seiner UUID, Seriennummer oder MAC-Adresse.','server',canOperate()?actionButton('Ersten Server hinzufügen','create-host','plus','',true):'')}`+(unassigned.length?`
Entdeckte Hardware ${unassigned.length}
Noch nicht zugeordnete Geräte erhalten keine Installationskonfiguration.
${runs.length?table(['LAUF','STATUS','FORTSCHRITT',''],runRows(runs)):empty('Noch keine Installationsläufe','Nach einer Freigabe und dem ersten ISO-Kontakt erscheint hier der zugehörige Lauf.','activity')}
`:`${empty(installation?'Noch keine Installationsprofile':'Noch keine Postinstallationsprofile',installation?'Legen Sie zuerst ein Root-Geheimnis und die Netzwerk- und Datenträgerkonfiguration Ihres Hardwaretyps an.':'Erstellen und veröffentlichen Sie zunächst Skriptmodule. Fassen Sie diese anschließend zu einem Ablauf zusammen.',installation?'layers':'workflow',canAuthor()?actionButton('Erstes Profil erstellen','create-profile','plus',`data-kind="${kind}"`,true):'')}`);
}
function modulesPage(modules) {
return header('Skriptmodule','Check, Apply und Verify: versionierte Bausteine für die Nachkonfiguration.',canAuthor()?actionButton('Aus Vorlage','module-catalog','layers')+actionButton('Modul erstellen','create-module','plus','',true):'','KONFIGURATION / SKRIPTE')+`${toolbar('Name oder Zielbuild suchen …',[],['draft','published'])}${modules.length?table(['MODUL','VERSION','STATUS','ZIELBUILDS','WIEDERHOLUNG',''],modules.map(m=>`
${svg('code')}
${esc(m.name)}${Number(m.timeout_seconds||300)} s Timeout
`)):empty('Ihre Konfiguration als Bausteine','Jedes Modul definiert Zustandsprüfung, Änderung und Erfolgskontrolle. Eine Veröffentlichung erfordert einen Testnachweis.','code',canAuthor()?actionButton('Erstes Modul erstellen','create-module','plus','',true):'')}`;
}
function runsPage(runs) {
return header('Installationsläufe','Installationen und Nachkonfigurationen vom ersten Kontakt bis zur Abschlussprüfung.',actionButton('Aktualisieren','refresh','refresh'),'VERWALTUNG / LÄUFE')+`${toolbar('Host oder Lauf-ID suchen …',[],[...new Set(runs.map(r=>r.status))])}${runs.length?table(['SERVER / LAUF','STATUS','FORTSCHRITT / START',''],runRows(runs)):empty('Noch keine Installationsläufe','Starten Sie einen freigegebenen Server mit dem registrierten Installationsmedium. Der Lauf wird beim Antwortabruf automatisch angelegt.','activity','Zum Serverinventar →')}`;
}
function runPage(run) {
const steps=arr(run.steps), events=arr(run.events), logs=arr(run.logs), terminal=['succeeded','failed','cancelled','canceled','expired'].includes(run.status);
const resumeAllowed=['needs_review','waiting_retry'].includes(run.status) && !run.cancel_requested;
const actions=`${svg('back')}Alle Läufe${canOperate()&&resumeAllowed?actionButton('Wiederaufnehmen','resume-run','play',`data-id="${esc(run.id)}"`,true):''}${canOperate()&&!terminal?actionButton('Abbrechen','cancel-run','stop',`data-id="${esc(run.id)}"`)+actionButton('Lauf abgleichen','reconcile-run','shield',`data-id="${esc(run.id)}"`):''}`;
let content='';
if(state.runTab==='steps') content=steps.length?`${steps.map((s,i)=>`
`).join('')}`:empty('Noch keine Schritte gemeldet','Die fixierten Schritte erscheinen mit dem Start der Nachkonfiguration.','workflow');
if(state.runTab==='events') content=eventList(events,'Noch keine Laufereignisse');
if(state.runTab==='logs') content=logs.length?`
`:empty('Noch keine Protokolldaten','Der Runner übermittelt redigierte Protokolle während der Ausführung.','code');
return header(run.host_fqdn||run.fqdn||`Lauf ${shortId(run.id)}`,`Lauf ${run.id}`,actions,'INSTALLATIONSLAUF')+`
`;
}
function mediaPage(records, groups=[]) {
return header('Installationsmedien','Gemeinsame ISO-Medien registrieren, Zugriffe begrenzen und Kompatibilität belegen.',(canAdmin()?actionButton('Gruppentoken erstellen','create-group','key')+actionButton('ISO registrieren','create-iso','plus','',true):''),'KONFIGURATION / MEDIEN')+
`
Die ISO wird auf einer Build-Maschine mit dem Proxmox Auto Install Assistant vorbereitet. Registrieren Sie anschließend den konkreten Build mit Prüfsumme und Testnachweis.
Registrierte ISO-Medien ${records.length}
Freigabe ausschließlich für die dokumentierte Build-Kombination
`)):empty('Noch keine Installationsmedien','Erstellen Sie zuerst eine Bereitstellungsgruppe. Registrieren Sie danach Ihr geprüftes ISO-Medium.','disc',canAdmin()?actionButton('ISO registrieren','create-iso','plus','',true):'')}
Bereitstellungsgruppen
Zeitlich begrenzte Gruppentoken für den Antwortabruf
`)):empty('Keine Gruppen vorhanden','Ein Gruppentoken berechtigt zum Antwortabruf für zugeordnete und freigegebene Hosts.','key',canAdmin()?actionButton('Gruppe erstellen','create-group','plus'):'')}
`;
}
function auditPage(events) {
return header('Auditprotokoll','Änderungen, Freigaben und privilegierte Zugriffe nachvollziehen.',actionButton('Aktualisieren','refresh','refresh'),'SYSTEM / AUDIT')+`${toolbar('Aktion, Benutzer oder Objekt suchen …')}${events.length?table(['ZEITPUNKT','AKTEUR','AKTION','OBJEKT',''],events.map(e=>`
`)):empty('Noch keine Auditereignisse','Änderungen und Freigaben werden mit Akteur, Zeitpunkt und Änderungsgrund aufgezeichnet.','shield')}`;
}
function settingsPage(users=[], secrets=[]) {
if(!canAdmin()) return header('Einstellungen','Ihre Zugriffsrechte im Workspace.','','SYSTEM / EINSTELLUNGEN')+`
Ihr Konto
Benutzername
${esc(state.me.username)}
Rolle
${esc(roleNames[state.me.role]||state.me.role)}
Benutzer und Geheimnisse werden durch Administratoren verwaltet.
`)):empty('Keine Benutzer gefunden','Legen Sie ein Benutzerkonto mit der passenden Rolle an.','users')}
Leser sehen redigierte Daten. Operatoren verwalten Hosts und Läufe. Skriptautoren erstellen Entwürfe. Administratoren verwalten Freigaben, Benutzer und Geheimnisse. Entwickler haben alle Berechtigungen.
`:`
Geheimnisse werden verschlüsselt gespeichert und über ihre ID referenziert. Der gespeicherte Wert wird in der Konsole nicht erneut angezeigt.
`)):empty('Noch keine Geheimnisse hinterlegt','Hinterlegen Sie beispielsweise den Root-Passwort-Hash für ein Installationsprofil.','key',actionButton('Geheimnis hinterlegen','create-secret','key','',true))}`);
}
function showModal(title, content, submit=null, eyebrow='PROXMOX AIS') {
document.getElementById('modal-title').textContent=title;
document.getElementById('modal-eyebrow').textContent=eyebrow;
document.getElementById('modal-body').innerHTML=content;
modalSubmit=submit;
if(!modal.open) modal.showModal();
}
function closeModal() {modal.close();modalSubmit=null;document.getElementById('modal-body').replaceChildren();}
function field(name,label,value='',options={}) {
const attrs=`name="${esc(name)}"${options.required?' required':''}${options.placeholder?` placeholder="${esc(options.placeholder)}"`:''}${options.min!==undefined?` min="${esc(options.min)}"`:''}${options.max!==undefined?` max="${esc(options.max)}"`:''}${options.autocomplete?` autocomplete="${esc(options.autocomplete)}"`:''}`;
let input;
if(options.type==='textarea') input=``;
else if(options.type==='select') input=``;
else if(options.type==='checkbox') return ``;
else input=``;
return ``;
}
function form(fields,label='Speichern',intro='') {
return `${intro}`;
}
const split = value => String(value||'').split(/[,\n]/).map(s=>s.trim()).filter(Boolean);
const selectObjects = (objects, emptyLabel='Bitte auswählen') => [{value:'',label:emptyLabel},...objects.map(o=>({value:o.id,label:`${o.name || o.fqdn}${o.version?` · v${o.version}`:''}${o.build?` · ${o.build}`:''}`}))];
async function hostForm(existing=null, discovery=null) { return graphicalHostForm(existing,discovery); }
async function hostImportForm() { return graphicalHostImport(); }
async function moduleCatalog() {
const catalog=arr(await api('/modules/builtin'));
showModal('Basismodul als Entwurf übernehmen',`
Die Vorlagen sind Ausgangspunkte. Prüfen Sie die Parameter, tragen Sie den konkreten Zielbuild ein und dokumentieren Sie vor Veröffentlichung einen Test.
Die Installation überschreibt die ausgewählten Systemdatenträger. Prüfen Sie Host, Netzwerk, Zielbuild und Datenträger vor der Freigabe.
Server
${esc(host.fqdn)}
Management-IP
${esc(host.management_ip)}
Standort
${esc(host.site)}
${previewContent(preview)}`;
const fields=field('confirmation','Hostnamen zur Bestätigung eingeben','',{required:true,full:true,placeholder:host.fqdn})+field('valid_minutes','Freigabefenster in Minuten',30,{type:'number',required:true,min:5,max:240})+field('reason','Freigabegrund','',{required:true,placeholder:'Geplante Erstinstallation'})+field('disks_confirmed','Ich habe die Zieldatenträger geprüft und bestätige, dass diese überschrieben werden dürfen.',false,{type:'checkbox',required:true,full:true});
showModal('Installation freigeben',form(fields,'Verbindlich freigeben',intro),async data=>{
if(data.get('confirmation')!==host.fqdn)throw new Error('Der eingegebene Hostname stimmt nicht mit dem Server überein.');
await api(`/hosts/${encodeURIComponent(id)}/approve-install`,{method:'POST',body:{expected_version:host.version,valid_minutes:Number(data.get('valid_minutes')),confirmation:data.get('confirmation'),disks_confirmed:data.get('disks_confirmed')==='on',reason:data.get('reason')}});
closeModal();toast('Installationsfreigabe erteilt. Der Server kann mit der zugewiesenen ISO gestartet werden.');await refresh();
},'ZEITLICH BEGRENZTE INSTALLATIONSFREIGABE');
}
async function profileForm(kind, existing=null) { return graphicalProfileForm(kind,existing); }
async function moduleForm(existing=null) { return graphicalModuleForm(existing); }
async function publishObject(type, id) {
const item=arr(state.data).find(x=>x.id===id) || await api(`/${type}/${encodeURIComponent(id)}`);
const intro=`
${esc(item.name)} · Version ${esc(item.version)} Veröffentlichten Inhalt können Sie nicht mehr ändern. Dokumentieren Sie den praktischen Test auf einem passenden Testhost. Im Vieraugenmodus muss eine andere Person den Entwurf veröffentlichen.
`;
showModal(type==='profiles'?'Profil veröffentlichen':'Modul veröffentlichen',form(field('test_evidence','Praktischer Testnachweis','',{type:'textarea',required:true,full:true,placeholder:'Testhost, Build, Datum, Ergebnisse und Referenz zum Prüfprotokoll'})+field('reason','Änderungsgrund','',{type:'textarea',required:true,full:true}),'Veröffentlichen',intro),async data=>{
await api(`/${type}/${encodeURIComponent(id)}/publish`,{method:'POST',body:{test_evidence:data.get('test_evidence'),reason:data.get('reason')}});closeModal();toast('Version veröffentlicht.');await refresh();
},'VERÖFFENTLICHUNG');
}
function inspectProfile(p) {
showModal(p.name,`
Version
v${esc(p.version)} ${badge(p.status)}
Zielbuilds
${esc((p.target_builds||[]).join(', ')||'–')}
Profil-ID
${esc(p.id)}
Digest
${esc(p.digest||'Noch nicht veröffentlicht')}
${dataView({values:p.values,steps:p.steps})}
${canAuthor()?``:''}
`,null,'PROFILDETAILS');
}
function inspectModule(m) {
showModal(m.name,`
Version / Status
v${esc(m.version)} ${badge(m.status)}
Modul-ID
${esc(m.id)}
Zielbuilds
${esc((m.target_builds||[]).join(', '))}
Digest
${esc(m.digest||'Noch nicht veröffentlicht')}
Testnachweis
${esc(m.test_evidence||'Noch nicht hinterlegt')}
${m.source?`
${esc(m.source)}
`:'
Der Quelltext ist für Skriptautoren und Administratoren sichtbar.
'}Parameterschema & Abhängigkeiten${dataView({parameters_schema:m.parameters_schema,dependencies:m.dependencies,retry_safe:m.retry_safe,timeout_seconds:m.timeout_seconds})}`,null,'MODULDETAILS');
}
async function isoForm() {
const groups=arr(await api('/groups'));
const fields=field('name','Medienname','',{required:true,placeholder:'Berlin · Proxmox VE'})+field('build','Exakter Proxmox ISO-Build','',{required:true,placeholder:'z. B. 9.1-1'})+field('sha256','SHA256-Prüfsumme des ISO-Mediums','',{required:true,full:true,placeholder:'64 hexadezimale Zeichen'})+field('assistant_version','Version des Auto Install Assistant','',{required:true,placeholder:'Exakte Paketversion'})+field('group_id','Bereitstellungsgruppe','',{required:true,type:'select',options:selectObjects(groups.filter(g=>!g.revoked))})+field('fingerprint','SHA256-Zertifikatsfingerprint','',{required:true,full:true,placeholder:'Fingerprint des HTTPS-Zertifikats des Antwortdienstes'})+field('test_status','Prüfstatus','draft',{type:'select',options:[{value:'draft',label:'Entwurf – noch nicht freigegeben'},{value:'passed',label:'Geprüft – Nachweis liegt vor'}]})+field('native_token_support','Native Unterstützung von --answer-auth-token ist nachgewiesen.',false,{type:'checkbox',required:true,full:true})+field('test_evidence','Kompatibilitätsnachweis','',{type:'textarea',full:true,placeholder:'Antwortschema, Token-Header, First Boot, Startnetz und Bootverfahren: Testhost, Datum und Prüfprotokoll.'});
showModal('ISO-Medium registrieren',form(fields,'Medium registrieren',groups.length?'':'
Erstellen Sie zuerst eine Bereitstellungsgruppe im Bereich Installationsmedien.
${esc(iso.test_evidence||'Noch nicht hinterlegt')}
Vorbereiteter Buildbefehl
SOURCE.iso und den Token-Platzhalter durch Ihre Eingaben ersetzen. Die URL muss aus dem Provisionierungsnetz per HTTPS erreichbar sein.
`,null,'ISO-DETAILS');
}
function groupForm() {
showModal('Bereitstellungsgruppe erstellen',form(field('name','Gruppenname','',{required:true,placeholder:'berlin-rack-a'})+field('site','Standort','',{required:true,placeholder:'Rechenzentrum Berlin'})+field('valid_hours','Gültigkeit in Stunden',24,{type:'number',required:true,min:1,max:8760,full:true}),'Token erstellen'),async data=>{
const result=await api('/groups',{method:'POST',body:{name:data.get('name'),site:data.get('site'),valid_hours:Number(data.get('valid_hours'))}});
showModal('Gruppentoken erstellt',`
Der vollständige Token wird nur jetzt angezeigt. Speichern Sie ihn für die ISO-Vorbereitung. Er ist auf die Bereitstellungsgruppe und deren Gültigkeitsfenster beschränkt.
Gruppe
${esc(result.name)}
Referenz-ID
${esc(result.id)}
Gültig bis
${esc(fmtDate(result.expires_at))}
Token für --answer-auth-token
${result.command?`
Buildbefehl
${esc(result.command)}
`:''}`,null,'EINMALIGE TOKENAUSGABE');
await refresh();
},'ISO-ZUGRIFF');
}
function userForm() {
showModal('Benutzer anlegen',form(field('username','Benutzername','',{required:true,autocomplete:'off'})+field('role','Rolle','reader',{type:'select',options:Object.entries(roleNames).map(([value,label])=>({value,label}))})+field('password','Initiales Passwort','',{required:true,type:'password',full:true,autocomplete:'new-password',hint:'Mindestens 12 Zeichen verwenden.'}),'Benutzer anlegen'),async data=>{
await api('/users',{method:'POST',body:{username:data.get('username'),password:data.get('password'),role:data.get('role')}});closeModal();toast('Benutzerkonto angelegt.');await refresh();
},'BENUTZER & ROLLEN');
}
function secretForm() {
const fields=field('name','Bezeichnung','',{required:true,full:true,placeholder:'Root-Zugang · PVE Berlin'})+field('kind','Verwendung','root_password',{type:'select',full:true,options:[{value:'root_password',label:'Root-Passwort für die Proxmox-Installation'},{value:'value',label:'Anderes Geheimnis oder vorhandener Passwort-Hash'}]})+field('value','Passwort / Geheimnis','',{required:true,type:'password',full:true,autocomplete:'new-password',hint:'Für ein neues Root-Passwort mindestens 12 Zeichen eingeben. Das Tool bereitet es für den Installer auf.'});
showModal('Zugang hinterlegen',form(fields,'Verschlüsselt speichern','
Den gespeicherten Zugang anschließend im Profil oder Schritt nach seinem Namen auswählen.
'),async data=>{
if(data.get('kind')==='root_password'&&String(data.get('value')).length<12)throw new Error('Das Root-Passwort benötigt mindestens 12 Zeichen.');
await api('/secrets',{method:'POST',body:{name:data.get('name'),value:data.get('value'),kind:data.get('kind')}});closeModal();toast('Zugang gespeichert und in den Auswahllisten verfügbar.');state.settingsTab='secrets';await refresh();
},'BETRIEBSGEHEIMNIS');
}
async function runAction(id, action) {
const run=await api(`/runs/${encodeURIComponent(id)}`), resume=action==='resume';
showModal(resume?'Lauf wiederaufnehmen':'Lauf abbrechen',form(field('reason',resume?'Begründung und durchgeführte Prüfung':'Abbruchgrund','',{type:'textarea',full:true,required:true}),resume?'Wiederaufnahme anfordern':'Abbruch anfordern',`
${resume?'Der Runner prüft gespeicherte Checkpoints und Modulzustände vor weiteren Änderungen. Unklare, nicht wiederholbare Schritte benötigen eine manuelle Klärung.':'Der Runner stoppt am nächsten sicheren Übergang. Eine bereits gestartete Datenträgeroperation oder ein Paketmanager wird dadurch nicht rückgängig gemacht.'}
`),async data=>{
await api(`/runs/${encodeURIComponent(id)}/${action}`,{method:'POST',body:{reason:data.get('reason'),expected_version:run.version}});closeModal();toast(resume?'Wiederaufnahme angefordert.':'Abbruch angefordert.');await refresh();
},`LAUF ${shortId(id)}`);
}
async function reconcileRun(id) {
const run=await api(`/runs/${encodeURIComponent(id)}`);
const host=await api(`/hosts/${encodeURIComponent(run.host_id)}`);
const fields=field('reason','Durchgeführte Prüfung und Abgleichgrund','',{type:'textarea',full:true,required:true})+field('confirmation','Hostnamen zur Bestätigung eingeben','',{required:true,full:true,placeholder:host.fqdn})+field('execution_stopped','Ich habe am Host geprüft, dass Installer und Runner gestoppt sind.',false,{type:'checkbox',full:true,required:true});
showModal('Lauf manuell abgleichen',form(fields,'Lauf als abgebrochen schließen',`
Dieser Abgleich schließt einen aufgegebenen oder nach Wiederherstellung unklaren Lauf. Prüfen Sie zuvor direkt am Host, dass keine Ausführung mehr stattfindet. Bereits erteilte Laufberechtigungen und das Antwortfenster müssen abgelaufen sein. Für eine neue Installation ist eine neue Freigabe erforderlich.
`),async data=>{
if(data.get('confirmation')!==host.fqdn)throw new Error('Der eingegebene Hostname stimmt nicht mit dem Server überein.');
await api(`/runs/${encodeURIComponent(id)}/reconcile`,{method:'POST',body:{expected_version:run.version,reason:data.get('reason'),confirmation:data.get('confirmation'),execution_stopped:data.get('execution_stopped')==='on'}});closeModal();toast('Lauf abgeglichen und als abgebrochen geschlossen.');await refresh();
},host.fqdn);
}
async function renderRoute(showLoading=true) {
const route=window.location.hash.replace(/^#\/?/,'').split('/');
const pages=['dashboard','hosts','installation','postinstall','modules','runs','media','audit','settings'];
state.page=pages.includes(route[0])?route[0]:'dashboard';state.id=route[1]?decodeURIComponent(route[1]):null;
const version=++state.routeVersion;
const labels={dashboard:'Übersicht',hosts:'Serverinventar',installation:'Installationsprofile',postinstall:'Postinstallation',modules:'Skriptmodule',runs:'Installationsläufe',media:'Installationsmedien',audit:'Auditprotokoll',settings:'Einstellungen'};
document.getElementById('breadcrumb').textContent=labels[state.page];document.title=`${labels[state.page]} · Proxmox AIS`;
document.querySelectorAll('[data-nav]').forEach(a=>{a.classList.toggle('active',a.dataset.nav===state.page);if(a.dataset.nav===state.page)a.setAttribute('aria-current','page');else a.removeAttribute('aria-current');});
if(showLoading)main.innerHTML='
Daten werden geladen …
';
try {
let output, data;
if(state.page==='dashboard'){data=await api('/dashboard');output=dashboard(data);}
if(state.page==='hosts'){if(state.id){data=await api(`/hosts/${encodeURIComponent(state.id)}`);output=hostPage(data);}else{const results=await Promise.all([api('/hosts'),api('/discoveries')]);data={hosts:arr(results[0]),discoveries:arr(results[1])};output=hostsPage(data.hosts,data.discoveries);}}
if(['installation','postinstall'].includes(state.page)){data=arr(await api('/profiles'));output=profilesPage(data,state.page);}
if(state.page==='modules'){data=arr(await api('/modules'));output=modulesPage(data);}
if(state.page==='runs'){data=await api(state.id?`/runs/${encodeURIComponent(state.id)}`:'/runs');output=state.id?runPage(data):runsPage(arr(data));}
if(state.page==='media'){const result=await Promise.all([api('/iso-records'),(canOperate()||canAuthor())?api('/groups'):Promise.resolve([])]);data={records:arr(result[0]),groups:arr(result[1])};output=mediaPage(data.records,data.groups);}
if(state.page==='audit'){data=arr(await api('/audit'));output=auditPage(data);}
if(state.page==='settings'){data=canAdmin()?await Promise.all([api('/users'),api('/secrets')]):[[],[]];output=settingsPage(arr(data[0]),arr(data[1]));}
if(version!==state.routeVersion)return;
state.data=data; main.innerHTML=output;
if(state.page==='media'&&!canOperate()&&!canAuthor())main.querySelector('.groups-card')?.remove();
applyFilters();
document.getElementById('connection').className='connection';document.getElementById('connection').innerHTML=' Verbunden';
}catch(error){
if(version!==state.routeVersion)return;
document.getElementById('connection').className='connection disconnected';document.getElementById('connection').innerHTML=' Abruf fehlgeschlagen';
if(showLoading)main.innerHTML=header('Daten konnten nicht geladen werden','Prüfen Sie Ihre Verbindung und Zugriffsrechte.',actionButton('Erneut versuchen','refresh','refresh'))+`