<div class="small muted" style="margin:.65rem 0 .8rem 0;">Diese Sonderladeroutinen sind in der ETL-Jobdatenbank vorhanden, konnten aber keinem aktuell gelieferten Modul aus der Komponentenliste zugeordnet werden.</div>
@ -691,9 +748,14 @@ var TOOLTIP_UPDATE='Laden der Rohdaten (Hauptladeroutine)';
var TOOLTIP_UNLOAD_UPDATE='Unload und Update hintereinander';
var TOOLTIP_UNLOAD_UPDATE='Unload und Update hintereinander';
var TOOLTIP_UPGRADE='Modulaktualisierung (Upgrade der Datenstrukturen auf den ausgelieferten Stand)';
var TOOLTIP_UPGRADE='Modulaktualisierung (Upgrade der Datenstrukturen auf den ausgelieferten Stand)';
var DB_VERSION_FALLBACK=<%= dbVersionFallbackJson %>;
var DB_VERSION_FALLBACK=<%= dbVersionFallbackJson %>;
var DB_SPECIAL_JOBS=<%= dbSpecialJobsJson %>;
var AVAILABLE_VERSION_FALLBACK=<%= availableVersionFallbackJson %>;
var AVAILABLE_VERSION_FALLBACK=<%= availableVersionFallbackJson %>;
var modules=[];
var modules=[];
var jobsByComponent={};
var jobsByComponent={};
var expandedSpecials={};
var moduleLoadGeneration=0;
var SPECIAL_CACHE_KEY='superx.komponentenverwaltung.specials.v01';
var specialConnectorCache=loadSpecialConnectorCache();
var jobErrorsByComponent={};
var jobErrorsByComponent={};
var currentExecutionId=null;
var currentExecutionId=null;
var currentLogId=null;
var currentLogId=null;
@ -701,7 +763,7 @@ var lastStatusPayload=null;
var currentJobLabel=null;
var currentJobLabel=null;
var pollTimer=null;
var pollTimer=null;
var logFailureByExecutionId={};
var logFailureByExecutionId={};
var el={message:document.getElementById('message'),confirmModal:document.getElementById('confirmModal'),confirmTitle:document.getElementById('confirmTitle'),confirmBody:document.getElementById('confirmBody'),confirmCloseBtn:document.getElementById('confirmCloseBtn'),confirmCancelBtn:document.getElementById('confirmCancelBtn'),confirmOkBtn:document.getElementById('confirmOkBtn'),searchInput:document.getElementById('searchInput'),filterSelect:document.getElementById('filterSelect'),reloadBtn:document.getElementById('reloadBtn'),moduleCount:document.getElementById('moduleCount'),moduleTbody:document.getElementById('moduleTbody'),diagnosticsBtn:document.getElementById('diagnosticsBtn'),diagnosticsPanel:document.getElementById('diagnosticsPanel'),refreshDiagnosticsBtn:document.getElementById('refreshDiagnosticsBtn'),closeDiagnosticsBtn:document.getElementById('closeDiagnosticsBtn'),diagnosticsRest:document.getElementById('diagnosticsRest'),diagnosticsVersions:document.getElementById('diagnosticsVersions'),detailsPanel:document.getElementById('detailsPanel'),detailsTitle:document.getElementById('detailsTitle'),detailsBody:document.getElementById('detailsBody'),closeDetailsBtn:document.getElementById('closeDetailsBtn'),refreshRunBtn:document.getElementById('refreshRunBtn'),stopRunBtn:document.getElementById('stopRunBtn'),runId:document.getElementById('runId'),runJob:document.getElementById('runJob'),runStatus:document.getElementById('runStatus'),progressBar:document.getElementById('progressBar'),progressText:document.getElementById('progressText'),loadLogBtn:document.getElementById('loadLogBtn'),clearLogBtn:document.getElementById('clearLogBtn'),logOutput:document.getElementById('logOutput'),logHint:document.getElementById('logHint')};
var el={message:document.getElementById('message'),confirmModal:document.getElementById('confirmModal'),confirmTitle:document.getElementById('confirmTitle'),confirmBody:document.getElementById('confirmBody'),confirmCloseBtn:document.getElementById('confirmCloseBtn'),confirmCancelBtn:document.getElementById('confirmCancelBtn'),confirmOkBtn:document.getElementById('confirmOkBtn'),searchInput:document.getElementById('searchInput'),filterSelect:document.getElementById('filterSelect'),reloadModulesBtn:document.getElementById('reloadModulesBtn'),moduleCount:document.getElementById('moduleCount'),moduleTbody:document.getElementById('moduleTbody'),unassignedSpecialsBox:document.getElementById('unassignedSpecialsBox'),unassignedSpecialsCount:document.getElementById('unassignedSpecialsCount'),unassignedSpecialsBody:document.getElementById('unassignedSpecialsBody'),diagnosticsBtn:document.getElementById('diagnosticsBtn'),diagnosticsPanel:document.getElementById('diagnosticsPanel'),refreshDiagnosticsBtn:document.getElementById('refreshDiagnosticsBtn'),closeDiagnosticsBtn:document.getElementById('closeDiagnosticsBtn'),diagnosticsRest:document.getElementById('diagnosticsRest'),diagnosticsVersions:document.getElementById('diagnosticsVersions'),detailsPanel:document.getElementById('detailsPanel'),detailsTitle:document.getElementById('detailsTitle'),detailsBody:document.getElementById('detailsBody'),closeDetailsBtn:document.getElementById('closeDetailsBtn'),runPanel:document.getElementById('runPanel'),refreshRunBtn:document.getElementById('refreshRunBtn'),stopRunBtn:document.getElementById('stopRunBtn'),runId:document.getElementById('runId'),runJob:document.getElementById('runJob'),runStatus:document.getElementById('runStatus'),progressBar:document.getElementById('progressBar'),progressText:document.getElementById('progressText'),loadLogBtn:document.getElementById('loadLogBtn'),clearLogBtn:document.getElementById('clearLogBtn'),logOutput:document.getElementById('logOutput'),logHint:document.getElementById('logHint')};
function esc(v){if(v===null||v===undefined){return '';}return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}
function esc(v){if(v===null||v===undefined){return '';}return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}
function attr(v){return esc(v).replace(/`/g,'`');}
function attr(v){return esc(v).replace(/`/g,'`');}
function code(v){return '<span class="code">'+esc(v)+'</span>';}
function code(v){return '<span class="code">'+esc(v)+'</span>';}
@ -718,6 +780,7 @@ function askConfirm(title,bodyHtml,okLabel){
@ -751,6 +814,9 @@ function actionConfirmBody(actionLabel,moduleName,abbr,hint,extraHtml){
}
}
function setLog(t,h){el.logOutput.textContent=t||'';el.logHint.textContent=h||'';}
function setLog(t,h){el.logOutput.textContent=t||'';el.logHint.textContent=h||'';}
function appendLog(t){var old=el.logOutput.textContent||'';if(old==='Noch kein Lauf gestartet.'){old='';}el.logOutput.textContent=old+(old?'\n':'')+t;}
function appendLog(t){var old=el.logOutput.textContent||'';if(old==='Noch kein Lauf gestartet.'){old='';}el.logOutput.textContent=old+(old?'\n':'')+t;}
function scrollToRunArea(){if(!el.runPanel){return;}window.requestAnimationFrame(function(){el.runPanel.scrollIntoView({behavior:'smooth',block:'start'});});}
function runViewportAnchor(){return el.runPanel?el.runPanel.getBoundingClientRect().top:null;}
function restoreRunViewport(anchor){if(anchor===null||anchor===undefined||!el.runPanel){return;}var delta=el.runPanel.getBoundingClientRect().top-anchor;if(Math.abs(delta)>0.5){window.scrollBy(0,delta);}}
function extractLogText(data){if(data===null||data===undefined){return '';}if(typeof data==='string'){return data;}if(typeof data==='object'){if(data.log!==undefined&&data.log!==null){return String(data.log);}if(data.content!==undefined&&data.content!==null){return String(data.content);}if(data.message!==undefined&&data.message!==null){return String(data.message);}return stringify(data);}return String(data);}function extractLogLabel(data,id){if(data&&typeof data==='object'&&data.jobAbbreviation){return 'Log für JobExecutionId '+id+' ('+data.jobAbbreviation+')';}return 'Log für JobExecutionId '+id;}function fetchText(url,opt){var o=opt||{};o.credentials='same-origin';return fetch(url,o).then(function(r){return r.text().then(function(t){if(!r.ok){throw new Error('HTTP '+r.status+' bei '+url+(t?': '+t.substring(0,500):''));}return t;});});}
function extractLogText(data){if(data===null||data===undefined){return '';}if(typeof data==='string'){return data;}if(typeof data==='object'){if(data.log!==undefined&&data.log!==null){return String(data.log);}if(data.content!==undefined&&data.content!==null){return String(data.content);}if(data.message!==undefined&&data.message!==null){return String(data.message);}return stringify(data);}return String(data);}function extractLogLabel(data,id){if(data&&typeof data==='object'&&data.jobAbbreviation){return 'Log für JobExecutionId '+id+' ('+data.jobAbbreviation+')';}return 'Log für JobExecutionId '+id;}function fetchText(url,opt){var o=opt||{};o.credentials='same-origin';return fetch(url,o).then(function(r){return r.text().then(function(t){if(!r.ok){throw new Error('HTTP '+r.status+' bei '+url+(t?': '+t.substring(0,500):''));}return t;});});}
function fetchJson(url,opt){return fetchText(url,opt).then(function(t){if(!t||!t.trim()){return null;}try{return JSON.parse(t);}catch(e){return t;}});}
function fetchJson(url,opt){return fetchText(url,opt).then(function(t){if(!t||!t.trim()){return null;}try{return JSON.parse(t);}catch(e){return t;}});}
function key(m){return String(m.abbreviation||m.id||m.name||'');}
function key(m){return String(m.abbreviation||m.id||m.name||'');}
@ -770,6 +836,99 @@ function findJobById(arr,id){for(var i=0;i<arr.length;i++){if(String(arr[i].id||
function findJobBySuffix(arr,suffix){for(var i=0;i<arr.length;i++){var id=String(arr[i].id||'');if(id.length>=suffix.length&&id.substring(id.length-suffix.length)===suffix){return arr[i];}}return null;}
function findJobBySuffix(arr,suffix){for(var i=0;i<arr.length;i++){var id=String(arr[i].id||'');if(id.length>=suffix.length&&id.substring(id.length-suffix.length)===suffix){return arr[i];}}return null;}
function findJobContains(arr,part){for(var i=0;i<arr.length;i++){if(String(arr[i].id||'').indexOf(part)>=0){return arr[i];}}return null;}
function findJobContains(arr,part){for(var i=0;i<arr.length;i++){if(String(arr[i].id||'').indexOf(part)>=0){return arr[i];}}return null;}
function sourceDb(m,j){var id=String(j&&j.id?j.id:'');var c=m.connectors||[];for(var i=0;i<c.length;i++){if(String(c[i].id||'')===id&&c[i].sourceDatabase){return c[i].sourceDatabase;}}if(m.database){return m.database;}return 'eduetl';}
function sourceDb(m,j){var id=String(j&&j.id?j.id:'');var c=m.connectors||[];for(var i=0;i<c.length;i++){if(String(c[i].id||'')===id&&c[i].sourceDatabase){return c[i].sourceDatabase;}}if(m.database){return m.database;}return 'eduetl';}
function loadSpecialConnectorCache(){try{var raw=window.sessionStorage?sessionStorage.getItem(SPECIAL_CACHE_KEY):null;if(!raw){return {};}var parsed=JSON.parse(raw);return parsed&&typeof parsed==='object'?parsed:{};}catch(e){return {};}}
function saveSpecialConnectorCache(){try{if(window.sessionStorage){sessionStorage.setItem(SPECIAL_CACHE_KEY,JSON.stringify(specialConnectorCache));}}catch(e){}}
function clearSpecialConnectorCache(){specialConnectorCache={};try{if(window.sessionStorage){sessionStorage.removeItem(SPECIAL_CACHE_KEY);}}catch(e){}}
function rawSpecialConnectors(m){var c=(m&&Array.isArray(m.connectors))?m.connectors:[],out=[];for(var i=0;i<c.length;i++){var x=c[i]||{},id=String(x.id||x.jobId||x.uniquename||''),type=String(x.jobType||x.type||'').toLowerCase();if(id&&(type==='special'||/_special$/.test(id))){out.push(x);}}return out;}
function inferSpecialOwnerKey(x,parentKey){
if(!x){return cleanModuleKey(parentKey||'');}
var explicit=['moduleAbbreviation','module_abbreviation','moduleId','module_id','componentId','component_id','component','module'];
for(var owner in specialConnectorCache){if(Object.prototype.hasOwnProperty.call(specialConnectorCache,owner)&&Array.isArray(specialConnectorCache[owner])){for(var c=0;c<specialConnectorCache[owner].length;c++){add(specialConnectorCache[owner][c],owner);}}}
function specialSourceDb(m,c){if(c&&c.sourceDatabase!==undefined&&c.sourceDatabase!==null&&String(c.sourceDatabase).trim()!==''){return String(c.sourceDatabase).trim();}if(c&&c.database!==undefined&&c.database!==null&&String(c.database).trim()!==''){return String(c.database).trim();}if(m&&m.database!==undefined&&m.database!==null&&String(m.database).trim()!==''){return String(m.database).trim();}return 'eduetl';}
function findSpecialConnector(m,id){var c=specialConnectors(m),wanted=String(id||'');for(var i=0;i<c.length;i++){if(String(c[i].id||'')===wanted){return c[i];}}return null;}
function displayDataSource(m){var raw=(m&&m.database!==undefined&&m.database!==null&&String(m.database).trim()!=='')?String(m.database):'eduetl / keine Datenquelle gesetzt';var normalized=raw.toLowerCase().replace(/[\s_\-\/]/g,'');if(normalized.indexOf('eduetl')>=0){return 'SuperX';}return raw;}
function displayDataSource(m){var raw=(m&&m.database!==undefined&&m.database!==null&&String(m.database).trim()!=='')?String(m.database):'eduetl / keine Datenquelle gesetzt';var normalized=raw.toLowerCase().replace(/[\s_\-\/]/g,'');if(normalized.indexOf('eduetl')>=0){return 'SuperX';}return raw;}
function isDeliveredVersion(v){if(v===null||v===undefined){return false;}var s=String(v).trim();if(!s){return false;}var n=s.toLowerCase();return n!=='nicht geliefert'&&n!=='null'&&n!=='undefined';}
function isDeliveredVersion(v){if(v===null||v===undefined){return false;}var s=String(v).trim();if(!s){return false;}var n=s.toLowerCase();return n!=='nicht geliefert'&&n!=='null'&&n!=='undefined';}
@ -783,14 +942,148 @@ function unloadPlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),id=abbr+'_unloa
function updateOnlyPlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),id=abbr+'_load_and_transform';if(m.installed!==true){return notInstalledPlan(m,id);}var j=findJobById(arr,id)||findJobBySuffix(arr,'_load_and_transform')||findJobContains(arr,'load_and_transform');if(j){return {available:true,job:j,sourceDatabase:sourceDb(m,j),endpoint:'/job/execute/'+sourceDb(m,j)+'/'+j.id,reason:'Gefunden über /job/list/'+abbr+'. Dieser Job führt Load und Transform aus und setzt in der Regel bereits vorhandene oder zuvor entladene Rohdaten voraus.',searched:id};}if(flagTrue(m.load)){return inferredPlan(m,id,'Über /job/list/'+abbr+' wurde kein passender Update-Job gefunden. /etl/list meldet jedoch load=true; deshalb wird der Standardjob '+id+' als Fallback angeboten.',id);}return {available:false,job:null,sourceDatabase:null,endpoint:null,reason:reasonForMissing(m,err,arr,'kein einzelner Update-Job gefunden. Gesucht wurde '+id+' beziehungsweise *_load_and_transform.'),searched:id};}
function updateOnlyPlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),id=abbr+'_load_and_transform';if(m.installed!==true){return notInstalledPlan(m,id);}var j=findJobById(arr,id)||findJobBySuffix(arr,'_load_and_transform')||findJobContains(arr,'load_and_transform');if(j){return {available:true,job:j,sourceDatabase:sourceDb(m,j),endpoint:'/job/execute/'+sourceDb(m,j)+'/'+j.id,reason:'Gefunden über /job/list/'+abbr+'. Dieser Job führt Load und Transform aus und setzt in der Regel bereits vorhandene oder zuvor entladene Rohdaten voraus.',searched:id};}if(flagTrue(m.load)){return inferredPlan(m,id,'Über /job/list/'+abbr+' wurde kein passender Update-Job gefunden. /etl/list meldet jedoch load=true; deshalb wird der Standardjob '+id+' als Fallback angeboten.',id);}return {available:false,job:null,sourceDatabase:null,endpoint:null,reason:reasonForMissing(m,err,arr,'kein einzelner Update-Job gefunden. Gesucht wurde '+id+' beziehungsweise *_load_and_transform.'),searched:id};}
function upgradePlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),installUpgrade=findJobById(arr,abbr+'_install_upgrade')||findJobBySuffix(arr,'_install_upgrade'),vi=versionInfo(m),isInstall=m.installed!==true,endpoint='/job/upgrade/'+abbr,ds=null;var r=isInstall?'Das Modul ist noch nicht installiert. Wenn der technische *_install_upgrade-Job verfügbar ist, wird dieser direkt über /job/execute gestartet.':(m.needsUpgrade===true?'Die Komponentenliste meldet needsUpgrade=true. Ein Upgradebedarf wurde erkannt.':'Die Komponentenliste meldet keinen Upgradebedarf. Das Upgrade kann trotzdem bewusst erneut ausgeführt werden.');if(err){r+=' Hinweis: Die technische Jobliste konnte nicht geladen werden: '+err;}else if(installUpgrade&&installUpgrade.id){ds=sourceDb(m,installUpgrade);endpoint='/job/execute/'+ds+'/'+installUpgrade.id;r+=' In der technischen Jobliste wurde '+installUpgrade.id+' gefunden. Dieser technische Install-/Upgrade-Job wird direkt gestartet; das ist robuster als der allgemeine Komponenten-Endpunkt /job/upgrade/{modul}.';}else{r+=' In der technischen Jobliste wurde kein *_install_upgrade-Job gefunden; gestartet wird weiterhin der Komponenten-Endpunkt.';}return {available:!!abbr,endpoint:endpoint,job:installUpgrade,sourceDatabase:ds,reason:r,versionInfo:vi,actionLabel:isInstall?'Installieren':'Upgrade'};}
function upgradePlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),installUpgrade=findJobById(arr,abbr+'_install_upgrade')||findJobBySuffix(arr,'_install_upgrade'),vi=versionInfo(m),isInstall=m.installed!==true,endpoint='/job/upgrade/'+abbr,ds=null;var r=isInstall?'Das Modul ist noch nicht installiert. Wenn der technische *_install_upgrade-Job verfügbar ist, wird dieser direkt über /job/execute gestartet.':(m.needsUpgrade===true?'Die Komponentenliste meldet needsUpgrade=true. Ein Upgradebedarf wurde erkannt.':'Die Komponentenliste meldet keinen Upgradebedarf. Das Upgrade kann trotzdem bewusst erneut ausgeführt werden.');if(err){r+=' Hinweis: Die technische Jobliste konnte nicht geladen werden: '+err;}else if(installUpgrade&&installUpgrade.id){ds=sourceDb(m,installUpgrade);endpoint='/job/execute/'+ds+'/'+installUpgrade.id;r+=' In der technischen Jobliste wurde '+installUpgrade.id+' gefunden. Dieser technische Install-/Upgrade-Job wird direkt gestartet; das ist robuster als der allgemeine Komponenten-Endpunkt /job/upgrade/{modul}.';}else{r+=' In der technischen Jobliste wurde kein *_install_upgrade-Job gefunden; gestartet wird weiterhin der Komponenten-Endpunkt.';}return {available:!!abbr,endpoint:endpoint,job:installUpgrade,sourceDatabase:ds,reason:r,versionInfo:vi,actionLabel:isInstall?'Installieren':'Upgrade'};}
var installUpgradeLabel=m.installed===true?'Upgrade':'Installieren',installUpgradeTooltip=m.installed===true?TOOLTIP_UPGRADE:'Modulinstallation durchführen';
el.moduleCount.textContent=shown+' von '+modules.length+' Modulen angezeigt'+(unassignedCount?' · '+unassignedCount+' nicht zugeordnete Sonderladeroutine'+(unassignedCount===1?'':'n'):'');
bindButtons();
}
function bindButtons(){
var bs=document.querySelectorAll('.js-complete');for(var i=0;i<bs.length;i++){bs[i].onclick=function(){startPlan(findModule(this.getAttribute('data-component')),completePlan,'Unload & Update');};}
function showDetails(m){if(!m){return;}var abbr=key(m),arr=jobs(m),vi=versionInfo(m),cp=completePlan(m),ul=unloadPlan(m),uo=updateOnlyPlan(m),ug=upgradePlan(m),h='';el.detailsTitle.textContent='Details: '+(m.name||abbr);h+='<div class="details-grid"><div class="box"><h3>Modul</h3><div class="kv"><div class="k">Name</div><div>'+esc(m.name||'-')+'</div><div class="k">Kürzel</div><div>'+code(abbr||'-')+'</div><div class="k">Systeminfo-ID</div><div>'+esc(m.systeminfoId||'-')+'</div><div class="k">Installiert</div><div>'+(m.installed===true?'ja':'nein')+'</div><div class="k">Installierte Version</div><div>'+versionValue(vi.installed)+'</div><div class="k">Verfügbare Version</div><div>'+versionValue(vi.target)+'</div><div class="k">Datenquelle</div><div>'+esc(displayDataSource(m))+'</div><div class="k">DB verbunden</div><div>'+(m.databaseConnected===true?'ja':(m.databaseConnected===false?'nein':'unbekannt'))+'</div><div class="k">Letztes Update</div><div>'+fmtDate(m.lastUpdate)+'</div></div></div><div>';h+=renderPlan('Unload',ul);h+=renderPlan('Update',uo);h+=renderPlan('Unload & Update',cp);var iuLabel=ug.actionLabel||'Upgrade';h+='<div class="box"><h3>'+esc(iuLabel)+'</h3><div class="plan-grid"><div class="muted">Bezeichnung</div><div>'+(m.installed===true?'Modulaktualisierung':'Modulinstallation')+'</div><div class="muted">verfügbar</div><div>'+(ug.available?badge('ja','ok'):badge('nein','warn'))+'</div><div class="muted">Upgradebedarf</div><div>'+(m.installed===true?(m.needsUpgrade===true?badge('laut /etl/list ja','warn'):badge('laut /etl/list nein','neutral')):'<span class="muted">nicht installiert</span>')+'</div><div class="muted">Start</div><div>'+code(ug.endpoint)+'</div><div class="muted">technischer Job</div><div>'+(ug.job&&ug.job.id?code(ug.job.id):'<span class="muted">nicht gefunden / nicht benötigt</span>')+'</div><div class="muted">Erklärung</div><div>'+esc(ug.reason)+'</div></div></div></div></div>';h+='<div class="box"><h3>Technische Jobs laut REST-Schnittstelle</h3>';if(jobErr(m)){h+='<div class="message error">Jobliste konnte nicht geladen werden: '+esc(jobErr(m))+'</div>';}else if(arr.length===0){h+='<div class="muted">Für dieses Modul wurden keine technischen Jobs zurückgeliefert.</div>';}else{h+='<table class="table is-fullwidth is-striped"><thead><tr><th>Job-ID</th><th>Name</th><th>Typ</th></tr></thead><tbody>';for(var i=0;i<arr.length;i++){h+='<tr><td>'+code(arr[i].id||'-')+'</td><td>'+esc(arr[i].name||'-')+'</td><td>'+esc(arr[i].type||'-')+'</td></tr>';}h+='</tbody></table>';}h+='</div><details class="box"><summary>JSON dieses Moduls anzeigen</summary><pre class="json">'+esc(JSON.stringify(m,null,2))+'</pre></details><details class="box"><summary>Komplette JSON-Liste aus /etl/list anzeigen</summary><pre class="json">'+esc(JSON.stringify(modules,null,2))+'</pre></details><details class="box"><summary>JSON der technischen Jobs dieses Moduls anzeigen</summary><pre class="json">'+esc(JSON.stringify(arr,null,2))+'</pre></details>';el.detailsBody.innerHTML=h;el.detailsPanel.classList.remove('hidden');el.detailsPanel.scrollIntoView({behavior:'smooth',block:'start'});}
function showDetails(m){if(!m){return;}var abbr=key(m),arr=jobs(m),vi=versionInfo(m),cp=completePlan(m),ul=unloadPlan(m),uo=updateOnlyPlan(m),ug=upgradePlan(m),h='';el.detailsTitle.textContent='Details: '+(m.name||abbr);h+='<div class="details-grid"><div class="box"><h3>Modul</h3><div class="kv"><div class="k">Name</div><div>'+esc(m.name||'-')+'</div><div class="k">Kürzel</div><div>'+code(abbr||'-')+'</div><div class="k">Systeminfo-ID</div><div>'+esc(m.systeminfoId||'-')+'</div><div class="k">Installiert</div><div>'+(m.installed===true?'ja':'nein')+'</div><div class="k">Installierte Version</div><div>'+versionValue(vi.installed)+'</div><div class="k">Verfügbare Version</div><div>'+versionValue(vi.target)+'</div><div class="k">Datenquelle</div><div>'+esc(displayDataSource(m))+'</div><div class="k">DB verbunden</div><div>'+(m.databaseConnected===true?'ja':(m.databaseConnected===false?'nein':'unbekannt'))+'</div><div class="k">Letztes Update</div><div>'+fmtDate(m.lastUpdate)+'</div></div></div><div>';h+=renderPlan('Unload',ul);h+=renderPlan('Update',uo);h+=renderPlan('Unload & Update',cp);var iuLabel=ug.actionLabel||'Upgrade';h+='<div class="box"><h3>'+esc(iuLabel)+'</h3><div class="plan-grid"><div class="muted">Bezeichnung</div><div>'+(m.installed===true?'Modulaktualisierung':'Modulinstallation')+'</div><div class="muted">verfügbar</div><div>'+(ug.available?badge('ja','ok'):badge('nein','warn'))+'</div><div class="muted">Upgradebedarf</div><div>'+(m.installed===true?(m.needsUpgrade===true?badge('laut /etl/list ja','warn'):badge('laut /etl/list nein','neutral')):'<span class="muted">nicht installiert</span>')+'</div><div class="muted">Start</div><div>'+code(ug.endpoint)+'</div><div class="muted">technischer Job</div><div>'+(ug.job&&ug.job.id?code(ug.job.id):'<span class="muted">nicht gefunden / nicht benötigt</span>')+'</div><div class="muted">Erklärung</div><div>'+esc(ug.reason)+'</div></div></div></div></div>';h+='<div class="box"><h3>Technische Jobs laut REST-Schnittstelle</h3>';if(jobErr(m)){h+='<div class="message error">Jobliste konnte nicht geladen werden: '+esc(jobErr(m))+'</div>';}else if(arr.length===0){h+='<div class="muted">Für dieses Modul wurden keine technischen Jobs zurückgeliefert.</div>';}else{h+='<table class="table is-fullwidth is-striped"><thead><tr><th>Job-ID</th><th>Name</th><th>Typ</th></tr></thead><tbody>';for(var i=0;i<arr.length;i++){h+='<tr><td>'+code(arr[i].id||'-')+'</td><td>'+esc(arr[i].name||'-')+'</td><td>'+esc(arr[i].type||'-')+'</td></tr>';}h+='</tbody></table>';}h+='</div><details class="box"><summary>JSON dieses Moduls anzeigen</summary><pre class="json">'+esc(JSON.stringify(m,null,2))+'</pre></details><details class="box"><summary>Komplette JSON-Liste aus /etl/list anzeigen</summary><pre class="json">'+esc(JSON.stringify(modules,null,2))+'</pre></details><details class="box"><summary>JSON der technischen Jobs dieses Moduls anzeigen</summary><pre class="json">'+esc(JSON.stringify(arr,null,2))+'</pre></details>';el.detailsBody.innerHTML=h;el.detailsPanel.classList.remove('hidden');el.detailsPanel.scrollIntoView({behavior:'smooth',block:'start'});}
function loadModules(){hideMsg();el.moduleTbody.innerHTML='<tr><td colspan="7" class="muted">Module werden geladen...</td></tr>';el.moduleCount.textContent='';var installedPromise=fetchJson(API_BASE+'/list');var installablesPromise=fetchJson(API_BASE+'/installables/list').catch(function(e){appendLog('Hinweis: /installables/list konnte nicht geladen werden: '+(e.message||String(e)));return [];});return Promise.all([installedPromise,installablesPromise]).then(function(results){var data=results[0],installables=results[1];if(!Array.isArray(data)){throw new Error('Unerwartete Antwort von /etl/list');}if(!Array.isArray(installables)){installables=[];}modules=mergeModuleLists(data,installables);jobsByComponent={};jobErrorsByComponent={};renderModules();renderVersionDiagnostics();return loadAllJobs().then(function(){renderModules();renderVersionDiagnostics();msg('Module und Jobinformationen wurden geladen.','ok');});}).catch(function(e){modules=[];jobsByComponent={};jobErrorsByComponent={};el.moduleTbody.innerHTML='<tr><td colspan="7" class="muted">Module konnten nicht geladen werden.</td></tr>';msg(e.message||String(e),'error');});}
function loadModules(options){
function loadAllJobs(){var ps=[];for(var i=0;i<modules.length;i++){ps.push(loadJobList(modules[i]));}return Promise.all(ps);}
var keepTable=!!(options&&options.keepTable),preserveRunPosition=!!(options&&options.preserveRunPosition),runAnchor=preserveRunPosition?runViewportAnchor():null,generation=++moduleLoadGeneration;
function loadJobList(m){var abbr=key(m);if(!abbr){return Promise.resolve();}return fetchJson(API_BASE+'/job/list/'+encodeURIComponent(abbr)).then(function(data){if(Array.isArray(data)){jobsByComponent[abbr]=data;}else{jobsByComponent[abbr]=[];jobErrorsByComponent[abbr]='Unerwartete Antwort von /job/list/'+abbr;}}).catch(function(e){jobsByComponent[abbr]=[];jobErrorsByComponent[abbr]=e.message||String(e);});}
hideMsg();
if(!keepTable){el.moduleTbody.innerHTML='<tr><td colspan="7" class="muted">Module und Jobinformationen werden geladen...</td></tr>';el.moduleCount.textContent='';}
var installables=[];
return fetchJson(API_BASE+'/installables/list').catch(function(e){appendLog('Hinweis: /installables/list konnte nicht geladen werden: '+(e.message||String(e)));return [];}).then(function(data){
installables=Array.isArray(data)?data:[];
return fetchJson(API_BASE+'/list');
}).then(function(data){
if(generation!==moduleLoadGeneration){return;}
if(!Array.isArray(data)){throw new Error('Unerwartete Antwort von /etl/list');}
rememberSpecialConnectors(data);
modules=mergeModuleLists(data,installables);
jobsByComponent={};jobErrorsByComponent={};
renderVersionDiagnostics();
return loadAllJobs().then(function(){
if(generation!==moduleLoadGeneration){return;}
renderModules();restoreRunViewport(runAnchor);renderVersionDiagnostics();msg('Module und Jobinformationen wurden geladen.','ok');
function copyFromElement(id){var node=document.getElementById(id);if(!node){return;}var text=node.value!==undefined?node.value:node.textContent;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(text).then(function(){msg('Text wurde in die Zwischenablage kopiert.','ok');}).catch(function(){node.focus();if(node.select){node.select();}msg('Text markieren und manuell kopieren.','warn');});}else{node.focus();if(node.select){node.select();}msg('Text markieren und manuell kopieren.','warn');}}
function copyFromElement(id){var node=document.getElementById(id);if(!node){return;}var text=node.value!==undefined?node.value:node.textContent;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(text).then(function(){msg('Text wurde in die Zwischenablage kopiert.','ok');}).catch(function(){node.focus();if(node.select){node.select();}msg('Text markieren und manuell kopieren.','warn');});}else{node.focus();if(node.select){node.select();}msg('Text markieren und manuell kopieren.','warn');}}
function startUnassignedSpecialConnector(connectorId){
var wanted=String(connectorId||''),all=unassignedSpecialConnectors(),c=null;
if(!c){msg('Die ausgewählte Sonderladeroutine wurde nicht mehr gefunden. Bitte die Komponentenliste aktualisieren.','error');return;}
var id=String(c.id||''),owner=inferSpecialOwnerKey(c,c._ownerKey||''),db=unassignedExecutionDb(c),label=c.name||id;
var extra=confirmRow('Sonderladeroutine',label)+confirmRow('Zuordnung',owner||'kein Modul aus /etl/list','code-value')+confirmRow('Datenbank',db,'code-value')+confirmRow('Job-ID',id,'code-value');
var body=actionConfirmBody('Sonderladeroutine',owner?('Nicht zugeordnet ('+owner+')'):'Nicht zugeordnet',owner,'Die Sonderladeroutine ist in der ETL-Jobdatenbank vorhanden, konnte aber keinem aktuell gelieferten Modul zugeordnet werden. Sie wird direkt über ihre Job-ID gestartet. Bitte nur starten, wenn aktuell kein anderer ETL-Lauf aktiv ist.',extra);
function startSpecialConnector(m,connectorId){if(!m){return;}var c=findSpecialConnector(m,connectorId);if(!c){msg('Die ausgewählte Sonderladeroutine wurde nicht mehr gefunden. Bitte die Komponentenliste aktualisieren.','error');return;}var id=String(c.id||''),db=specialSourceDb(m,c),moduleName=m.name||key(m),label=c.name||id,endpoint='/job/execute/'+db+'/'+id;var extra=confirmRow('Sonderladeroutine',label)+confirmRow('Datenquelle',db,'code-value')+confirmRow('Job-ID',id,'code-value');if(c.description){extra+=confirmRow('Beschreibung',c.description);}var body=actionConfirmBody('Sonderladeroutine',moduleName,key(m),'Die Sonderladeroutine wird unabhängig von der normalen Modul-Laderoutine gestartet. Bitte nur starten, wenn aktuell kein anderer ETL-Lauf aktiv ist.',extra);askConfirm('Sonderladeroutine starten?',body,'Sonderladeroutine starten').then(function(ok){if(!ok){return;}startJob(API_BASE+endpoint,'Sonderladeroutine '+label+' ('+id+')');});}
function reloadJobsToDb(){
var body='<div class="content">'+confirmRow('Aktion','Module neu einlesen','action')+'<div class="notification is-warning is-light confirm-hint">Die Modul- und ETL-Jobdefinitionen werden aus dem Dateisystem neu in die Datenbank eingelesen. Anschließend wird die Übersicht automatisch aktualisiert.</div></div>';
askConfirm('Module neu einlesen?',body,'Neu einlesen').then(function(ok){
if(!ok){return;}
el.reloadModulesBtn.disabled=true;
hideMsg();
appendLog('Module neu einlesen: '+API_BASE+'/job/writeJobsToDb');
appendLog('Antwort Module neu einlesen: '+stringify(result));
if(result===false||String(result).toLowerCase()==='false'){throw new Error('Der Server hat das Neueinlesen mit false beantwortet.');}
expandedSpecials={};
clearSpecialConnectorCache();
window.location.reload();
return null;
}).catch(function(e){msg('Module konnten nicht neu eingelesen werden: '+(e.message||String(e)),'error');appendLog('Fehler beim Neueinlesen: '+(e.message||String(e)));el.reloadModulesBtn.disabled=false;});
});
}
function startPlan(m,planFn,label){if(!m){return;}var p=planFn(m);if(!p.available){msg(label+' ist nicht verfügbar: '+p.reason,'error');return;}var jobId=(p.job&&p.job.id)?p.job.id:'-';var moduleName=m.name||key(m);var body=actionConfirmBody(label,moduleName,key(m),'Der Vorgang kann länger dauern. Bitte nur starten, wenn aktuell kein anderer ETL-Lauf aktiv ist.');askConfirm(label+' starten?',body,label+' starten').then(function(ok){if(!ok){return;}startJob(API_BASE+p.endpoint,label+' '+moduleName+' ('+jobId+')');});}
function startPlan(m,planFn,label){if(!m){return;}var p=planFn(m);if(!p.available){msg(label+' ist nicht verfügbar: '+p.reason,'error');return;}var jobId=(p.job&&p.job.id)?p.job.id:'-';var moduleName=m.name||key(m);var body=actionConfirmBody(label,moduleName,key(m),'Der Vorgang kann länger dauern. Bitte nur starten, wenn aktuell kein anderer ETL-Lauf aktiv ist.');askConfirm(label+' starten?',body,label+' starten').then(function(ok){if(!ok){return;}startJob(API_BASE+p.endpoint,label+' '+moduleName+' ('+jobId+')');});}
function startUpgrade(m){if(!m){return;}var p=upgradePlan(m);var isInstall=m.installed!==true;var label=isInstall?'Installation':'Upgrade';if(!p.available){msg(label+' ist nicht verfügbar: '+p.reason,'error');return;}var abbr=key(m);var moduleName=m.name||abbr;var extra=confirmRow('Installierte Version',p.versionInfo.installed||'nicht geliefert','code-value')+confirmRow('Verfügbare Version',p.versionInfo.target||'nicht geliefert','code-value');var body=actionConfirmBody(label,moduleName,abbr,'Der Vorgang kann länger dauern. Bitte nur starten, wenn aktuell kein anderer ETL-/Installationslauf aktiv ist.',extra);askConfirm(isInstall?'Modul installieren?':'Upgrade starten?',body,label+' starten').then(function(ok){if(!ok){return;}startJob(API_BASE+p.endpoint,(isInstall?'Installieren ':'Upgrade ')+moduleName);});}
function startUpgrade(m){if(!m){return;}var p=upgradePlan(m);var isInstall=m.installed!==true;var label=isInstall?'Installation':'Upgrade';if(!p.available){msg(label+' ist nicht verfügbar: '+p.reason,'error');return;}var abbr=key(m);var moduleName=m.name||abbr;var extra=confirmRow('Installierte Version',p.versionInfo.installed||'nicht geliefert','code-value')+confirmRow('Verfügbare Version',p.versionInfo.target||'nicht geliefert','code-value');var body=actionConfirmBody(label,moduleName,abbr,'Der Vorgang kann länger dauern. Bitte nur starten, wenn aktuell kein anderer ETL-/Installationslauf aktiv ist.',extra);askConfirm(isInstall?'Modul installieren?':'Upgrade starten?',body,label+' starten').then(function(ok){if(!ok){return;}startJob(API_BASE+p.endpoint,(isInstall?'Installieren ':'Upgrade ')+moduleName);});}
function startJob(url,label){hideMsg();setLog('', 'Neuer Lauf wird gestartet.');appendLog('Starte: '+label);appendLog(url);fetchJson(url,{method:'POST'}).then(function(result){var id=parseId(result);if(id===-1){msg('Der Server hat -1 zurückgeliefert. Es wurde kein neuer Job gestartet. Mögliche Ursachen: Ein anderer Job läuft bereits oder der Server konnte für diesen Endpunkt keinen passenden Startjob ermitteln.','error');appendLog('Start nicht angenommen: -1');return checkRunning(true);}if(id===null){msg('Startantwort konnte nicht als JobExecutionId interpretiert werden.','error');appendLog('Startantwort: '+stringify(result));return;}currentExecutionId=id;currentLogId=null;lastStatusPayload=null;currentJobLabel=label;logFailureByExecutionId[id]=false;setLog('Job gestartet. Log wird nach Abschluss automatisch geladen oder kann über „Log laden“ abgefragt werden.','JobExecutionId '+id);updateRun('gestartet');el.loadLogBtn.disabled=false;el.stopRunBtn.disabled=false;msg('Job gestartet. JobExecutionId: '+id,'ok');return syncRunningAfterStart(id,label).then(function(){startPolling();});}).catch(function(e){msg(e.message||String(e),'error');appendLog('Fehler beim Start: '+(e.message||String(e)));});}
function startJob(url,label){hideMsg();setLog('', 'Neuer Lauf wird gestartet.');appendLog('Starte: '+label);appendLog(url);fetchJson(url,{method:'POST'}).then(function(result){var id=parseId(result);if(id===-1){msg('Der Server hat -1 zurückgeliefert. Es wurde kein neuer Job gestartet. Mögliche Ursachen: Ein anderer Job läuft bereits oder der Server konnte für diesen Endpunkt keinen passenden Startjob ermitteln.','error');appendLog('Start nicht angenommen: -1');return checkRunning(true);}if(id===null){msg('Startantwort konnte nicht als JobExecutionId interpretiert werden.','error');appendLog('Startantwort: '+stringify(result));return;}currentExecutionId=id;currentLogId=null;lastStatusPayload=null;currentJobLabel=label;logFailureByExecutionId[id]=false;setLog('Job gestartet. Log wird nach Abschluss automatisch geladen oder kann über „Log laden“ abgefragt werden.','JobExecutionId '+id);updateRun('gestartet');el.loadLogBtn.disabled=false;el.stopRunBtn.disabled=false;msg('Job gestartet. JobExecutionId: '+id,'ok');scrollToRunArea();return syncRunningAfterStart(id,label).then(function(){startPolling();});}).catch(function(e){msg(e.message||String(e),'error');appendLog('Fehler beim Start: '+(e.message||String(e)));});}
function syncRunningAfterStart(startId,label){return new Promise(function(resolve){setTimeout(resolve,700);}).then(function(){return fetchJson(API_BASE+'/running');}).then(function(data){var runningId=parseId(data);if(runningId&&runningId>0&&runningId!==startId){appendLog('Hinweis: /running meldet laufende JobExecutionId '+runningId+' statt Startantwort '+startId+'. Verwende '+runningId+' für Status und Log.');currentExecutionId=runningId;}if(data&&typeof data==='object'){updateKnownIds(data);if(data.jobName){currentJobLabel=data.jobName;}else if(data.name){currentJobLabel=data.name;}}updateRun('gestartet');return Promise.resolve();}).catch(function(e){appendLog('Hinweis: /running konnte nach dem Start nicht zur ID-Prüfung gelesen werden: '+(e.message||String(e)));return Promise.resolve();});}
function syncRunningAfterStart(startId,label){return new Promise(function(resolve){setTimeout(resolve,700);}).then(function(){return fetchJson(API_BASE+'/running');}).then(function(data){var runningId=parseId(data);if(runningId&&runningId>0&&runningId!==startId){appendLog('Hinweis: /running meldet laufende JobExecutionId '+runningId+' statt Startantwort '+startId+'. Verwende '+runningId+' für Status und Log.');currentExecutionId=runningId;}if(data&&typeof data==='object'){updateKnownIds(data);if(data.jobName){currentJobLabel=data.jobName;}else if(data.name){currentJobLabel=data.name;}}updateRun('gestartet');return Promise.resolve();}).catch(function(e){appendLog('Hinweis: /running konnte nach dem Start nicht zur ID-Prüfung gelesen werden: '+(e.message||String(e)));return Promise.resolve();});}
function stringify(v){if(typeof v==='string'){return v;}try{return JSON.stringify(v,null,2);}catch(e){return String(v);}}
function stringify(v){if(typeof v==='string'){return v;}try{return JSON.stringify(v,null,2);}catch(e){return String(v);}}
@ -823,7 +1144,7 @@ function logIdCandidates(){var ids=[];if(lastStatusPayload&&typeof lastStatusPay
function updateRun(s){el.runId.textContent=currentExecutionId||'-';el.runJob.textContent=currentJobLabel||'-';el.runStatus.textContent=s||'-';}
function updateRun(s){el.runId.textContent=currentExecutionId||'-';el.runJob.textContent=currentJobLabel||'-';el.runStatus.textContent=s||'-';}
function startPolling(){if(pollTimer){clearInterval(pollTimer);}pollStatus();pollTimer=setInterval(pollStatus,4000);}
function startPolling(){if(pollTimer){clearInterval(pollTimer);}pollStatus();pollTimer=setInterval(pollStatus,4000);}
function stopPolling(){if(pollTimer){clearInterval(pollTimer);pollTimer=null;}}
function stopPolling(){if(pollTimer){clearInterval(pollTimer);pollTimer=null;}}
function pollStatus(){if(!currentExecutionId){return Promise.resolve();}return fetchJson(API_BASE+'/jobExecutionStatus/'+encodeURIComponent(currentExecutionId)).then(function(data){updateKnownIds(data);renderStatus(data);var s=statusOf(data);if(doneStatus(s)){stopPolling();loadLog(false);loadModules();}}).catch(function(err){return fetchJson(API_BASE+'/jobExcecutionStatus/'+encodeURIComponent(currentExecutionId)).then(function(data){updateKnownIds(data);renderStatus(data);var s=statusOf(data);if(doneStatus(s)){stopPolling();loadLog(false);loadModules();}}).catch(function(){el.runStatus.textContent='Status konnte nicht gelesen werden: '+(err.message||String(err));});});}
function pollStatus(){if(!currentExecutionId){return Promise.resolve();}return fetchJson(API_BASE+'/jobExecutionStatus/'+encodeURIComponent(currentExecutionId)).then(function(data){updateKnownIds(data);renderStatus(data);var s=statusOf(data);if(doneStatus(s)){stopPolling();loadLog(false);loadModules({keepTable:true,preserveRunPosition:true});}}).catch(function(err){return fetchJson(API_BASE+'/jobExcecutionStatus/'+encodeURIComponent(currentExecutionId)).then(function(data){updateKnownIds(data);renderStatus(data);var s=statusOf(data);if(doneStatus(s)){stopPolling();loadLog(false);loadModules({keepTable:true,preserveRunPosition:true});}}).catch(function(){el.runStatus.textContent='Status konnte nicht gelesen werden: '+(err.message||String(err));});});}
function isUnknownStatus(s){var u=String(s||'').trim().toUpperCase();return u==='UNKNOWN'||u==='UNBEKANNT';}
function isUnknownStatus(s){var u=String(s||'').trim().toUpperCase();return u==='UNKNOWN'||u==='UNBEKANNT';}
function statusOf(d){
function statusOf(d){
if(!d){return '';}
if(!d){return '';}
@ -840,7 +1161,7 @@ function renderStatus(data){var s=statusOf(data);el.runStatus.textContent=displa
function loadLog(userRequested){if(!currentExecutionId&&!currentLogId){return Promise.resolve();}var candidates=logIdCandidates();if(candidates.length===0){return Promise.resolve();}var failureKey=candidates.join(',');if(logFailureByExecutionId[failureKey]&&userRequested!==true){return Promise.resolve();}el.logHint.textContent='Log wird geladen...';function tryCandidate(pos,errors){if(pos>=candidates.length){var text=errors.join('\n');logFailureByExecutionId[failureKey]=true;var hint='Log konnte für die bekannten IDs '+candidates.join(', ')+' nicht geladen werden.';el.logHint.textContent='Log nicht verfügbar.';if(userRequested===true){appendLog(hint);appendLog(text);appendLog('Hinweis: Der Log-Endpunkt verwendet je nach Backendstand eventuell eine andere ID als /jobExecutionStatus. Die Diagnose kann die aktuell bekannten REST-Antworten sichtbar machen.');}else{appendLog(hint+' Der Laufstatus wurde trotzdem abgefragt.');}return Promise.resolve();}var id=candidates[pos];return fetchJson(API_BASE+'/jobLog/'+encodeURIComponent(id)).then(function(data){currentLogId=id;logFailureByExecutionId[failureKey]=false;var t=extractLogText(data);if(t&&t.trim()){setLog(t, extractLogLabel(data,id));}else{setLog('(Der Log-Endpunkt hat keinen Logtext geliefert.)', extractLogLabel(data,id));}}).catch(function(e){errors.push('/jobLog/'+id+': '+(e.message||String(e)));return tryCandidate(pos+1,errors);});}
function loadLog(userRequested){if(!currentExecutionId&&!currentLogId){return Promise.resolve();}var candidates=logIdCandidates();if(candidates.length===0){return Promise.resolve();}var failureKey=candidates.join(',');if(logFailureByExecutionId[failureKey]&&userRequested!==true){return Promise.resolve();}el.logHint.textContent='Log wird geladen...';function tryCandidate(pos,errors){if(pos>=candidates.length){var text=errors.join('\n');logFailureByExecutionId[failureKey]=true;var hint='Log konnte für die bekannten IDs '+candidates.join(', ')+' nicht geladen werden.';el.logHint.textContent='Log nicht verfügbar.';if(userRequested===true){appendLog(hint);appendLog(text);appendLog('Hinweis: Der Log-Endpunkt verwendet je nach Backendstand eventuell eine andere ID als /jobExecutionStatus. Die Diagnose kann die aktuell bekannten REST-Antworten sichtbar machen.');}else{appendLog(hint+' Der Laufstatus wurde trotzdem abgefragt.');}return Promise.resolve();}var id=candidates[pos];return fetchJson(API_BASE+'/jobLog/'+encodeURIComponent(id)).then(function(data){currentLogId=id;logFailureByExecutionId[failureKey]=false;var t=extractLogText(data);if(t&&t.trim()){setLog(t, extractLogLabel(data,id));}else{setLog('(Der Log-Endpunkt hat keinen Logtext geliefert.)', extractLogLabel(data,id));}}).catch(function(e){errors.push('/jobLog/'+id+': '+(e.message||String(e)));return tryCandidate(pos+1,errors);});}
return tryCandidate(0,[]);}
return tryCandidate(0,[]);}
function checkRunning(verbose){return fetchJson(API_BASE+'/running').then(function(data){var id=parseId(data);if(id&&id>0){if(verbose){appendLog('Antwort /running: '+stringify(data));}currentExecutionId=id;updateKnownIds(data);currentJobLabel=(data&&data.jobName)?data.jobName:((data&&data.name)?data.name:'laufender Job');logFailureByExecutionId[id]=false;el.loadLogBtn.disabled=false;el.stopRunBtn.disabled=false;updateRun('laufender Job gefunden');startPolling();}else{if(verbose){appendLog('Kein laufender Job laut /running.');}if(!currentExecutionId){currentLogId=null;lastStatusPayload=null;el.runStatus.textContent='Kein aktiver Lauf bekannt.';el.runId.textContent='-';el.runJob.textContent='-';if(el.progressBar.tagName&&el.progressBar.tagName.toLowerCase()==='progress'){el.progressBar.value=0;}else{el.progressBar.style.width='0%';}el.progressText.textContent='-';}}}).catch(function(e){appendLog('Running-Status konnte nicht gelesen werden: '+(e.message||String(e)));});}function stopRun(){if(!currentExecutionId){return;}var body='<div class="content">'+confirmRow('Aktion','Lauf stoppen','action')+confirmRow('JobExecutionId',currentExecutionId,'code-value')+'<div class="notification is-danger is-light confirm-hint">Der laufende Job wird zum Stoppen angefordert.</div></div>';askConfirm('Aktuellen Job stoppen?',body,'Job stoppen').then(function(ok){if(!ok){return;}fetchJson(API_BASE+'/stop/'+encodeURIComponent(currentExecutionId)).then(function(data){appendLog('Stop-Antwort: '+stringify(data));pollStatus();}).catch(function(e){msg('Job konnte nicht gestoppt werden: '+(e.message||String(e)),'error');});});}
function checkRunning(verbose){return fetchJson(API_BASE+'/running').then(function(data){var id=parseId(data);if(id&&id>0){if(verbose){appendLog('Antwort /running: '+stringify(data));}currentExecutionId=id;updateKnownIds(data);currentJobLabel=(data&&data.jobName)?data.jobName:((data&&data.name)?data.name:'laufender Job');logFailureByExecutionId[id]=false;el.loadLogBtn.disabled=false;el.stopRunBtn.disabled=false;updateRun('laufender Job gefunden');startPolling();}else{if(verbose){appendLog('Kein laufender Job laut /running.');}if(!currentExecutionId){currentLogId=null;lastStatusPayload=null;el.runStatus.textContent='Kein aktiver Lauf bekannt.';el.runId.textContent='-';el.runJob.textContent='-';if(el.progressBar.tagName&&el.progressBar.tagName.toLowerCase()==='progress'){el.progressBar.value=0;}else{el.progressBar.style.width='0%';}el.progressText.textContent='-';}}}).catch(function(e){appendLog('Running-Status konnte nicht gelesen werden: '+(e.message||String(e)));});}function stopRun(){if(!currentExecutionId){return;}var body='<div class="content">'+confirmRow('Aktion','Lauf stoppen','action')+confirmRow('JobExecutionId',currentExecutionId,'code-value')+'<div class="notification is-danger is-light confirm-hint">Der laufende Job wird zum Stoppen angefordert.</div></div>';askConfirm('Aktuellen Job stoppen?',body,'Job stoppen').then(function(ok){if(!ok){return;}fetchJson(API_BASE+'/stop/'+encodeURIComponent(currentExecutionId)).then(function(data){appendLog('Stop-Antwort: '+stringify(data));pollStatus();}).catch(function(e){msg('Job konnte nicht gestoppt werden: '+(e.message||String(e)),'error');});});}