/* Proxmox AIS administration console. No client-side dependencies. */ 'use strict'; const icons = { dashboard: '', server: '', activity: '', layers: '', workflow: '', code: '', disc: '', shield: '', settings: '', plus: '', arrow: '', refresh: '', search: '', check: '', clock: '', alert: '', edit: '', key: '', users: '', lock: '', copy: '', back: '', stop: '', play: '', file: '', }; const svg = (name) => ``; const esc = (value) => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const json = value => JSON.stringify(value ?? {}, null, 2); const arr = value => Array.isArray(value) ? value : (value?.items || []); const toDate = value => new Date(typeof value==='number' && value<1e12 ? value*1000 : value); const fmtDate = value => value && !Number.isNaN(toDate(value).getTime()) ? new Intl.DateTimeFormat('de-DE', {dateStyle:'short',timeStyle:'short'}).format(toDate(value)) : 'Noch kein Kontakt'; const shortId = value => String(value || '–').slice(0, 10); const roleNames = {reader:'Leser',operator:'Operator',author:'Skriptautor',admin:'Administrator',developer:'Entwickler'}; const statusNames = {draft:'Entwurf',published:'Veröffentlicht',discovered:'Entdeckt',ready:'Bereit',prepared:'Freigegeben',approved:'Freigegeben',answer_served:'Antwort ausgeliefert',answer_delivered:'Antwort ausgeliefert',installing:'Installation',installed:'Installiert',installed_reported:'Basisinstallation gemeldet',bootstrapping:'Erster Start',enrolled:'Registriert',runner_ready:'Runner bereit',running:'In Ausführung',postinstall_running:'Nachkonfiguration',postinstalling:'Nachkonfiguration',waiting_retry:'Wiederaufnahme erwartet',reboot_pending:'Neustart erwartet',waiting_reboot:'Neustart erwartet',rebooting:'Neustart',succeeded:'Erfolgreich',failed:'Fehlgeschlagen',needs_review:'Prüfung nötig',unknown:'Kontakt unbekannt',cancelled:'Abgebrochen',canceled:'Abgebrochen',cancel_requested:'Abbruch angefordert',blocked:'Gesperrt',pending:'Ausstehend',checking:'Prüfen',applying:'Anwenden',verifying:'Validieren',skipped:'Übersprungen',active:'Aktiv',passed:'Geprüft',expired:'Abgelaufen',revoked:'Widerrufen'}; const greenStatuses = new Set(['succeeded','passed','published','active','ready']); const redStatuses = new Set(['failed','needs_review','blocked','revoked']); const orangeStatuses = new Set(['draft','pending','discovered','unknown','expired','waiting_reboot','waiting_retry','reboot_pending','cancel_requested']); const blueStatuses = new Set(['approved','prepared','answer_served','answer_delivered','installing','installed','bootstrapping','enrolled','runner_ready','running','postinstall_running','postinstalling','checking','applying','verifying','rebooting']); const badge = status => `${esc(statusNames[status] || status || 'Unbekannt')}`; const state = {me:null,page:'dashboard',id:null,data:null,routeVersion:0,settingsTab:'users',runTab:'steps',refreshing:false}; const canOperate = () => ['admin','developer','operator'].includes(state.me?.role); const canAuthor = () => ['admin','developer','author'].includes(state.me?.role); const canAdmin = () => ['admin','developer'].includes(state.me?.role); const main = document.getElementById('main'); const modal = document.getElementById('modal'); let modalSubmit = null; function errorMessage(detail) { if (typeof detail === 'string') return detail; if (Array.isArray(detail)) return detail.map(e => `${(e.loc || []).filter(v=>v!=='body').join('.')}: ${e.msg || json(e)}`).join('\n'); return detail?.message || json(detail); } async function api(path, options = {}) { const headers = {'Accept':'application/json', ...(options.headers || {})}; if (options.body !== undefined && typeof options.body !== 'string') { headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(options.body); } if (options.method && options.method !== 'GET') headers['X-CSRF-Token'] = state.me?.csrf_token || ''; const response = await fetch(path.startsWith('/auth/') ? path : `/api/v1${path}`, {...options, headers, credentials:'same-origin'}); if (response.status === 401) { window.location.href = '/login'; throw new Error('Ihre Sitzung ist abgelaufen. Bitte erneut anmelden.'); } const contentType = response.headers.get('content-type') || ''; const result = response.status === 204 ? null : contentType.includes('json') ? await response.json() : await response.text(); if (!response.ok) throw new Error(errorMessage(result?.errors || result?.detail || result?.message || result || `Anfrage fehlgeschlagen (${response.status})`)); return result; } function toast(message, isError=false) { const el=document.createElement('div'); el.className=`toast${isError?' error':''}`; el.textContent=message; const region=document.getElementById('toast-region');while(region.children.length>=3)region.firstElementChild.remove();region.append(el); setTimeout(()=>el.remove(), isError?9000:4500); } function actionButton(label, action, icon='plus', attrs='', primary=false) { if(action==='preview-host' && !canOperate() && !canAuthor())return ''; return ``; } function header(title, description, actions='', eyebrow='PROVISIONING CONSOLE') { return ``; } function empty(title, description, icon='server', action='') { return `
${svg(icon)}

${esc(title)}

${esc(description)}

${action}
`; } function table(headings, rows) { return `
${headings.map(h=>``).join('')}${rows.join('')}
${esc(h)}
`; } function toolbar(placeholder='Suchen …', sites=[], statuses=[]) { return `
${sites.length?``:''}${statuses.length?``:''}
`; } const searchAttrs = (value, site='', status='') => `data-search="${esc(String(value).toLowerCase())}" data-site="${esc(site)}" data-status="${esc(status)}"`; function applyFilters() { const query=(document.getElementById('list-search')?.value || '').toLowerCase(); const site=document.getElementById('site-filter')?.value || ''; const status=document.getElementById('status-filter')?.value || ''; let count=0, total=0; main.querySelectorAll('[data-search]').forEach(row=>{ total++; const visible=row.dataset.search.includes(query) && (!site || row.dataset.site===site) && (!status || row.dataset.status===status); row.hidden=!visible; if(visible) count++; }); const countEl=document.getElementById('filter-count'); if(countEl) countEl.textContent=`${count} ${count===1?'Eintrag':'Einträge'}${count!==total?` von ${total}`:''}`; let noResults=document.getElementById('no-filter-results'); if(!noResults && total) {noResults=document.createElement('div');noResults.id='no-filter-results';noResults.className='empty-state';noResults.textContent='Keine Einträge für diese Auswahl.';main.querySelector('.filterable')?.append(noResults);} if(noResults) noResults.hidden=count!==0; } function hostRows(hosts, compact=false) { return hosts.map(h=>`
${svg('server')}
${esc(h.fqdn || h.name || `Entdeckter Host ${shortId(h.id)}`)}${esc(h.management_ip || 'Keine Management-IP')}
${esc(h.site || '–')}${badge(h.blocked?'blocked':h.status)}${compact?'':`${(h.tags||[]).map(t=>`${esc(t)}`).join('') || ''}${esc(fmtDate(h.last_contact || h.last_seen))}`}Details ${svg('arrow')}`); } 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${progress}%`:esc(fmtDate(r.started_at || r.created_at)); return `
${svg('activity')}
${badge(r.cancel_requested && !['cancelled','succeeded','failed','expired'].includes(r.status)?'cancel_requested':r.status)}${r.contact_status==='unknown'?` ${badge('unknown')}`:''}${progressCell}Ansehen ${svg('arrow')}`; }); } function eventList(events, blank='Noch keine Ereignisse') { if(!events.length) return empty(blank,'Ereignisse erscheinen hier, sobald Sie Hosts und Konfigurationen verwalten.','activity'); return `
${events.map(e=>`
${svg(e.status==='failed'?'alert':'activity')}
${esc(e.message || e.action || e.type || e.event_type || 'Statusänderung')}

${esc(e.host_fqdn || e.actor || e.username || e.object_id || shortId(e.run_id))} · ${esc(fmtDate(e.created_at || e.timestamp))}

`).join('')}
`; } function dashboard(data) { const c=data.counts||{}, hosts=arr(data.hosts), runs=arr(data.runs), events=arr(data.recent_events); const metrics=[['Server gesamt',c.hosts||0,'Inventarisierte Hosts','server',''],['Bereit zur Installation',c.ready||0,'Freigegebene Server','shield','orange'],['Aktive Läufe',c.active||0,'Installation & Nachkonfiguration','activity','green'],['Prüfung erforderlich',c.needs_review||0,'Vorgänge mit Handlungsbedarf','alert','red']]; return header('Infrastruktur im Überblick','Installationen steuern. Konfigurationen prüfen. Den Überblick behalten.',actionButton('Aktualisieren','refresh','refresh')+(canOperate()?actionButton('Server hinzufügen','create-host','plus','',true):''),'WORKSPACE / ÜBERSICHT')+ `
${metrics.map(m=>`
${m[0]}${svg(m[3])}
${Number(m[1])}
${m[2]}
`).join('')}

Installationsläufe ${runs.length}

Die letzten Provisionierungen und ihr aktueller Stand

Alle Läufe ${svg('arrow')}
${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'):'')}

Serverinventar ${Number(c.hosts||0)}

Ihre zuletzt erfassten Server

Zum Inventar ${svg('arrow')}
${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')}
${c.hosts?'IHR PROVISIONIERUNGSABLAUF':'ERSTE SCHRITTE'}${svg('workflow')}

Letzte Ereignisse

Was sich in Ihrem Workspace verändert

${eventList(events.slice(0,5))}
`; } 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.

${badge('discovered')}
${table(['IDENTITÄTEN','STANDORT / BUILD','ABLEHNUNGSGRUND',''],unassigned.map(d=>`${arr(d.identities).map(i=>`
${esc(i.kind)}: ${esc(i.value)}
`).join('')}${esc(d.site)}
${esc(d.build)}${esc(d.reason)}${canOperate()?``:''}`))}
`:''); } function hostPage(host) { const id=esc(host.id), runs=arr(host.runs), identities=arr(host.identities); return header(host.fqdn||'Entdeckter Server',`${host.site||'Kein Standort'} · ${host.management_ip||'Keine Management-IP'}`,`${svg('back')}Inventar${canOperate()?actionButton('Bearbeiten','edit-host','edit',`data-id="${id}"`)+actionButton('Installation freigeben','approve-host','shield',`data-id="${id}"`,true):''}`,'SERVERDETAIL / '+shortId(host.id))+ `

Serverkonfiguration

${badge(host.blocked?'blocked':host.status)}
FQDN
${esc(host.fqdn||'–')}
Management-IP
${esc(host.management_ip||'–')}
Standort
${esc(host.site||'–')}
Tags
${(host.tags||[]).map(t=>`${esc(t)}`).join('')||'–'}
Letzter Kontakt
${esc(fmtDate(host.last_contact||host.last_seen))}
Versionsstand
${esc(host.version)}
${identities.length?`
${identities.map(i=>`
${esc({uuid:'System-UUID',serial:'Seriennummer',mac:'MAC-Adresse'}[i.kind]||i.kind)}
${esc(i.value)}
`).join('')}
`:'

Noch keine Identitäten hinterlegt.

'}

Installationshistorie

${runs.length}
${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')}

Profilzuordnung

${svg('layers')}
Installation
${esc(host.installation_profile_name||host.installation_profile_id||'Nicht zugewiesen')}
Postinstallation
${esc(host.postinstall_profile_name||host.postinstall_profile_id||'Nicht zugewiesen')}
ISO-Medium
${esc(host.iso_name||host.iso_id||'Nicht zugewiesen')}

Jeder Lauf bindet feste Profil- und Modulversionen. Spätere Änderungen wirken auf neue Läufe.

${actionButton('Aufgelöste Vorschau','preview-host','file',`data-id="${id}"`)}
${host.discovered_data?`

Erkannte Systemdaten

${dataView(host.discovered_data)}
`:''}

Hostüberschreibungen

${dataView(host.overrides)}
`; } function profilesPage(profiles, kind) { const installation=kind==='installation'; const list=profiles.filter(p=>p.kind===kind); return header(installation?'Installationsprofile':'Postinstallationsprofile',installation?'Sprache, Managementnetz und explizite Systemdatenträger versioniert definieren.':'Geprüfte Module in einen nachvollziehbaren Ablauf mit festen Versionen bringen.',canAuthor()?actionButton('Profil erstellen','create-profile','plus',`data-kind="${kind}"`,true):'',`KONFIGURATION / ${installation?'INSTALLATION':'POSTINSTALLATION'}`)+ (list.length?`
${toolbar('Profilname oder Zielbuild suchen …')}
${list.map(p=>`
${svg(installation?'layers':'workflow')}${badge(p.status)}

${esc(p.name)}

Version ${esc(p.version)} · ${installation?'Installationskonfiguration':`${arr(p.steps).length} Konfigurationsschritte`}

${(p.target_builds||[]).map(b=>`PVE ${esc(b)}`).join('')||'Keine Zielbuilds'}
${p.digest?`SHA256 ${esc(p.digest.slice(0,25))}…`:'Digest nach Veröffentlichung'}
${canAuthor()?``:''}${canAdmin()&&p.status==='draft'?``:''}
`).join('')}
`:`
${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
v${esc(m.version)}${badge(m.status)}${(m.target_builds||[]).map(b=>`${esc(b)}`).join('')}${m.retry_safe?'Explizit erlaubt':'Manuelle Prüfung'}
${canAuthor()?``:''}${canAdmin()&&m.status==='draft'?``:''}
`)):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)=>`
  1. ${s.status==='succeeded'?'✓':i+1}
    ${esc(s.name || s.step_id || s.id || `Schritt ${i+1}`)}

    Modul ${esc(s.module_name || s.module_id || '–')} · Versuch ${Number(s.attempt || s.attempts || 0)}${s.exit_code!=null?` · Exit ${Number(s.exit_code)}`:''}

    ${s.required===false?'

    Optionaler Schritt

    ':''}${s.error?`

    ${esc(s.error)}

    `:''}${s.verification&&Object.keys(s.verification).length?`
    Prüfergebnis${s.status==='failed'?' / Fehlerursache':''}${dataView(s.verification)}
    `:''}${s.checkpoint?`
    Checkpoint${dataView(s.checkpoint)}
    `:''}
    ${badge(s.status)}
  2. `).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?`
${esc(logs.map(l=>typeof l==='string'?l:`${l.created_at?fmtDate(l.created_at)+' ':''}${l.step_id?'['+l.step_id+'] ':''}${l.content||l.text||l.message||json(l)}`).join('\n'))}
`: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')+`

Ausführungsstatus

${badge(run.status)}
${[['steps','Schritte'],['events','Ereignisse'],['logs','Protokolle']].map(([id,label])=>``).join('')}
${run.error||run.error_reason?`
${esc(run.error||run.error_reason)}
`:''}
${content}

Laufdaten

Server
${esc(run.host_fqdn||shortId(run.host_id))} ↗
Gestartet
${esc(fmtDate(run.started_at||run.created_at))}
Abgeschlossen
${run.finished_at||run.completed_at?esc(fmtDate(run.finished_at||run.completed_at)):'–'}
Letzter Heartbeat
${esc(fmtDate(run.last_heartbeat||run.last_contact||run.last_seen))}
Versionsstand
${esc(run.version)}
Manifest-Digest
${esc(run.manifest_digest||'Noch nicht erstellt')}
Antwort-Digest
${esc(run.answer_digest||run.answer_sha256||'–')}

Fixierte Konfiguration

${svg('lock')}

Profil- und Skriptversionen dieses Laufs bleiben nach der Reservierung unverändert.

${dataView(run.manifest || run.snapshot || run.profiles || {installation_profile_id:run.installation_profile_id,postinstall_profile_id:run.postinstall_profile_id})}
`; } 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

${records.length?table(['MEDIUM','ZIELBUILD','ASSISTANT','TESTSTATUS',''],records.map(r=>`
${svg('disc')}
${esc(r.name)}${esc(shortId(r.sha256))}…
${esc(r.build)}${esc(r.assistant_version)}${badge(r.test_status)}`)):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

${groups.length?table(['GRUPPE','STANDORT','GÜLTIG BIS','STATUS'],groups.map(g=>`${esc(g.name)}
${esc(g.id)}${esc(g.site||'–')}${esc(fmtDate(g.expires_at))}${badge(g.revoked?'revoked':toDate(g.expires_at)`)):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=>`${esc(fmtDate(e.created_at||e.timestamp))}${esc(e.actor||e.username||e.actor_id||'System')}${esc(e.action)}${esc(e.object_type||e.target_type||'')} ${esc(shortId(e.object_id||e.target_id))}`)):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.

`; const tab=state.settingsTab; return header('Einstellungen','Benutzerkonten und verschlüsselte Betriebsgeheimnisse verwalten.',tab==='users'?actionButton('Benutzer anlegen','create-user','plus','',true):actionButton('Geheimnis hinterlegen','create-secret','key','',true),'SYSTEM / EINSTELLUNGEN')+`
`+(tab==='users'?`

Benutzerkonten ${users.length}

${users.length?table(['BENUTZER','ROLLE','ERSTELLT'],users.map(u=>`
${svg('users')}${esc(u.username)}${u.id===state.me.id?'Sie':''}
${esc(roleNames[u.role]||u.role)}${esc(fmtDate(u.created_at))}`)):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.

Geheimnisreferenzen ${secrets.length}

${secrets.length?table(['NAME','REFERENZ-ID','ERSTELLT'],secrets.map(s=>`
${svg('key')}${esc(s.name)}
${esc(s.id)}${esc(fmtDate(s.created_at))}`)):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.
${catalog.map(m=>`
${svg('code')}
${esc(m.name)}

${esc(m.description)}

`).join('')}
`,null,'MODULVORLAGEN'); state.catalog=catalog; } function previewContent(preview) { const p=preview||{}, warnings=arr(p.warnings); const network=p.resolved?.network||{}; return `${warnings.length?`
${warnings.map(w=>esc(typeof w==='string'?w:w.message||json(w))).join('
')}
`:''}
Aufgelöste Konfiguration & Herkunft${dataView({resolved:p.resolved,provenance:p.provenance,profiles:p.profiles,steps:p.steps})}
`; } async function approveHost(id, previewOnly=false) { const [host, preview]=await Promise.all([api(`/hosts/${encodeURIComponent(id)}`),api(`/hosts/${encodeURIComponent(id)}/preview`)]); if(previewOnly){showModal('Aufgelöste Konfiguration',previewContent(preview)+`
`,null,host.fqdn);return;} const intro=`
Die Installation überschreibt die ausgewählten Systemdatenträger. Prüfen Sie Host, Netzwerk, Zielbuild und Datenträger vor der Freigabe.
${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,`${dataView({values:p.values,steps:p.steps})}
${canAuthor()?``:''}
`,null,'PROFILDETAILS'); } function inspectModule(m) { showModal(m.name,`${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.
'),async data=>{ await api('/iso-records',{method:'POST',body:{name:data.get('name'),build:data.get('build'),sha256:data.get('sha256'),assistant_version:data.get('assistant_version'),group_id:data.get('group_id'),fingerprint:data.get('fingerprint'),test_status:data.get('test_status'),native_token_support:data.get('native_token_support')==='on',test_evidence:data.get('test_evidence')}});closeModal();toast('ISO-Medium registriert.');await refresh(); },'INSTALLATIONSMEDIUM'); } function buildCommand(iso, token=':') { if(iso.command)return iso.command; const shellQuote = value => "'"+String(value).replace(/'/g,"'\\''")+"'"; const base=(iso.answer_url||`${window.location.origin}/installer/v1/answer`); return `proxmox-auto-install-assistant prepare-iso SOURCE.iso \\\n --fetch-from http \\\n --url ${shellQuote(base)} \\\n --cert-fingerprint ${shellQuote(iso.fingerprint||'')} \\\n --answer-auth-token ${shellQuote(token)}`; } function inspectISO(iso) { showModal(iso.name,`

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))}
${result.command?`
${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'))+``; else throw error; } } async function refresh() { if(state.refreshing)return; state.refreshing=true; const filters=['list-search','site-filter','status-filter'].map(id=>[id,document.getElementById(id)?.value]); try{await renderRoute(false);for(const [id,value]of filters){const el=document.getElementById(id);if(el&&value!==undefined)el.value=value;}applyFilters();}finally{state.refreshing=false;} } async function handleAction(button) { const action=button.dataset.action,id=button.dataset.id; if(action==='close-modal'){closeModal();return;} if(action==='menu'){const open=document.getElementById('sidebar').classList.toggle('open');button.setAttribute('aria-expanded',String(open));return;} if(action==='logout'){await api('/auth/logout',{method:'POST'});window.location.href='/login';return;} if(action==='refresh'){await refresh();return;} if(action==='create-host'){await hostForm();return;} if(action==='import-hosts'){await hostImportForm();return;} if(action==='assign-discovery'){await hostForm(null,state.data.discoveries.find(d=>d.id===id));return;} if(action==='edit-host'){await hostForm(await api(`/hosts/${encodeURIComponent(id)}`));return;} if(action==='approve-host'||action==='preview-host'){await approveHost(id,action==='preview-host');return;} if(action==='toggle-host'){ const host=await api(`/hosts/${encodeURIComponent(id)}`); await api(`/hosts/${encodeURIComponent(id)}`,{method:'PATCH',body:{expected_version:host.version,blocked:!host.blocked}});toast(host.blocked?'Host entsperrt.':'Host gesperrt.');await refresh();return; } if(action==='create-profile'){await profileForm(button.dataset.kind);return;} if(action==='version-profile'){const p=arr(state.data).find(x=>x.id===id)||await api(`/profiles/${encodeURIComponent(id)}`);await profileForm(p.kind,p);return;} if(action==='view-profile'){const p=arr(state.data).find(x=>x.id===id)||await api(`/profiles/${encodeURIComponent(id)}`);inspectProfile(p);return;} if(action==='publish-profile'){await publishObject('profiles',id);return;} if(action==='create-module'){await moduleForm();return;} if(action==='module-catalog'){await moduleCatalog();return;} if(action==='use-module-template'){const selected=state.catalog.find(m=>m.id===id);await moduleForm({...selected,version:0});return;} if(action==='version-module'||action==='view-module'){const m=arr(state.data).find(x=>x.id===id)||await api(`/modules/${encodeURIComponent(id)}`);if(action==='version-module')await moduleForm(m);else inspectModule(m);return;} if(action==='publish-module'){await publishObject('modules',id);return;} if(action==='create-iso'){await isoForm();return;} if(action==='view-iso'){inspectISO(state.data.records.find(x=>x.id===id));return;} if(action==='create-group'){groupForm();return;} if(action==='create-user'){userForm();return;} if(action==='create-secret'){secretForm();return;} if(action==='settings-tab'){state.settingsTab=button.dataset.tab;await renderRoute(false);return;} if(action==='run-tab'){state.runTab=button.dataset.tab;main.innerHTML=runPage(state.data);return;} if(action==='resume-run'||action==='cancel-run'){await runAction(id,action==='resume-run'?'resume':'cancel');return;} if(action==='reconcile-run'){await reconcileRun(id);return;} if(action==='copy-value'){ const el=document.getElementById('copy-value'); try{await navigator.clipboard.writeText(el.value);toast('In die Zwischenablage kopiert.');}catch{el.focus();el.select();toast('Text markiert. Mit Strg+C kopieren.');}return; } if(action==='view-audit'){const e=arr(state.data).find(x=>String(x.id)===id);showModal('Auditereignis',`${dataView(e)}
`,null,e?.action||'AUDIT');} } document.addEventListener('click',async event=>{ const button=event.target.closest('[data-action]'); if(button){event.preventDefault();if(button.disabled)return;button.disabled=true;try{await handleAction(button);}catch(error){toast(error.message,true);}finally{button.disabled=false;}} if(event.target.closest('[data-nav]')){document.getElementById('sidebar').classList.remove('open');document.querySelector('[data-action="menu"]').setAttribute('aria-expanded','false');} }); document.addEventListener('input',event=>{if(event.target.id==='list-search')applyFilters();}); document.addEventListener('change',event=>{if(['site-filter','status-filter'].includes(event.target.id))applyFilters();}); document.addEventListener('submit',async event=>{ if(event.target.id!=='modal-form')return; event.preventDefault();if(!modalSubmit)return; const formEl=event.target, submit=formEl.querySelector('[type="submit"]'),errorEl=formEl.querySelector('.form-error'); submit.disabled=true;errorEl.textContent=''; try{await modalSubmit(new FormData(formEl));}catch(error){errorEl.textContent=error.message;errorEl.scrollIntoView({block:'nearest'});}finally{submit.disabled=false;} }); modal.addEventListener('cancel',()=>{modalSubmit=null;document.getElementById('modal-body').replaceChildren();}); window.addEventListener('hashchange',()=>{renderRoute();}); async function init() { document.querySelectorAll('[data-icon]').forEach(el=>el.insertAdjacentHTML('afterbegin',svg(el.dataset.icon))); try{ state.me=await api('/me'); document.getElementById('username').textContent=state.me.username; document.getElementById('user-role').textContent=roleNames[state.me.role]||state.me.role; document.getElementById('avatar').textContent=state.me.username.slice(0,2).toUpperCase(); await renderRoute(); setInterval(async()=>{ if(document.hidden||modal.open||state.refreshing||['INPUT','SELECT','TEXTAREA'].includes(document.activeElement?.tagName)||!['dashboard','runs','hosts'].includes(state.page))return; try{await refresh();}catch{/* The connection indicator reports refresh failures. */} },15000); }catch(error){main.innerHTML=``;} } init();