<div class="header"><h1>SuperX ETL Manager</h1><p>Aufgeräumte Modulübersicht für den täglichen Betrieb. Technische Details, Jobauswahl und JSON-Rohdaten sind über den Detailbereich erreichbar.</p></div>
<div class="header"><h1>SuperX Komponentenverwaltung</h1><p>Aufgeräumte Modulübersicht für den täglichen Betrieb. Technische Details, Jobauswahl und JSON-Rohdaten sind über den Detailbereich erreichbar.</p></div>
@ -673,10 +696,12 @@ var modules=[];
@@ -673,10 +696,12 @@ var modules=[];
var jobsByComponent={};
var jobErrorsByComponent={};
var currentExecutionId=null;
var currentLogId=null;
var lastStatusPayload=null;
var currentJobLabel=null;
var pollTimer=null;
var logFailureByExecutionId={};
var el={message:document.getElementById('message'),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'),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')};
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 code(v){return '<span class="code">'+esc(v)+'</span>';}
@ -684,11 +709,61 @@ function badge(t,type){var cls='is-light';if(type==='ok'){cls='is-success is-lig
@@ -684,11 +709,61 @@ function badge(t,type){var cls='is-light';if(type==='ok'){cls='is-success is-lig
function versionValue(v){if(v===null||v===undefined||v===''){return '<span class="muted">nicht geliefert</span>';}return code(v);}
function msg(t,type){el.message.textContent=t;el.message.className='message'+(type?' '+type:'');el.message.classList.remove('hidden');}
function hideMsg(){el.message.classList.add('hidden');}
function actionConfirmBody(actionLabel,moduleName,abbr,hint,extraHtml){
return '<div class="content">'
+confirmRow('Aktion',actionLabel,'action')
+confirmRow('Modul',moduleName||abbr||'','')
+confirmRow('Kürzel',abbr||'','code-value')
+(extraHtml||'')
+'<div class="notification is-warning is-light confirm-hint">'+esc(hint||'Der Vorgang kann länger dauern. Bitte nur starten, wenn aktuell kein anderer ETL-/Installationslauf aktiv ist.')+'</div>'
+'</div>';
}
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 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 key(m){return String(m.abbreviation||m.id||m.name||'');}
function firstValue(o,keys){if(!o){return null;}for(var i=0;i<keys.length;i++){var v=o[keys[i]];if(v!==undefined&&v!==null&&String(v).trim()!==''){return v;}}return null;}
function cleanModuleKey(v){if(v===undefined||v===null){return '';}var s=String(v).trim();if(!s){return '';}s=s.replace(/\\/g,'/');var parts=s.split('/').filter(function(p){return p&&p.length;});if(parts.length&&parts[parts.length-1].toLowerCase()==='version'&&parts.length>1){s=parts[parts.length-2];}else if(parts.length){s=parts[parts.length-1];}s=s.replace(/\.(xml|conf|properties|txt)$/i,'');s=s.toLowerCase();s=s.replace(/[^a-z0-9_]/g,'');return s;}
function installableName(m,abbr){if(m===undefined||m===null){return abbr||'';}if(typeof m!=='object'){return String(m||abbr||'').trim();}return String(firstValue(m,['displayName','componentName','component_name','caption','title','label','name','description'])||abbr||'').trim();}
function installableTargetVersion(m){if(!m||typeof m!=='object'){return null;}return firstValue(m,['targetVersion','availableVersion','latestVersion','packageVersion','fileVersion','newVersion','upgradeVersion','version','target_version','available_version','latest_version','package_version','file_version','new_version','upgrade_version']);}
function notInstalledPlan(m,searched){return {available:false,job:null,sourceDatabase:null,endpoint:null,reason:'Das Modul ist noch nicht installiert. Zuerst die Modulinstallation ausführen.',searched:searched};}
function flagTrue(v){return v===true||v===1||String(v||'').toLowerCase()==='true'||String(v||'')==='1';}
function inferredJob(id,name){return {type:'INFERRED',name:name||id,id:id,inferred:true};}
function inferredPlan(m,id,reason,searched){var j=inferredJob(id,id);var db=sourceDb(m,j);return {available:true,job:j,sourceDatabase:db,endpoint:'/job/execute/'+db+'/'+id,reason:reason,searched:searched||id};}
function jobs(m){return jobsByComponent[key(m)]||[];}
function jobErr(m){return jobErrorsByComponent[key(m)]||null;}
function findJobById(arr,id){for(var i=0;i<arr.length;i++){if(String(arr[i].id||'')===id){return arr[i];}}return null;}
@ -703,17 +778,17 @@ var jsonInstalled=first(installedKeys);var dbVersion=dbFallback();var installed=
@@ -703,17 +778,17 @@ var jsonInstalled=first(installedKeys);var dbVersion=dbFallback();var installed=
var jsonTarget=first(targetKeys);var ai=availableInfo();var xmlVersion=isDeliveredVersion(ai.xmlVersion)?String(ai.xmlVersion).trim():null;var versionFileVersion=isDeliveredVersion(ai.versionFileVersion)?String(ai.versionFileVersion).trim():null;var target=jsonTarget;var targetSource=jsonTarget?'json':'';if(!target&&xmlVersion){target=xmlVersion;targetSource='module_xml';}if(!target&&versionFileVersion){target=versionFileVersion;targetSource='version_file';}
function reasonForMissing(m,err,arr,text){if(err){return 'Die technische Jobliste konnte nicht geladen werden: '+err;}if(m.installed===false){return 'Das Modul ist laut Komponentenliste nicht installiert.';}if(arr.length===0){return 'Für dieses Modul wurden über /job/list/'+key(m)+' keine technischen Jobs zurückgeliefert.';}return 'In der technischen Jobliste wurde '+text;}
function completePlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),id=abbr+'_unload_load_transform';var j=findJobById(arr,id)||findJobBySuffix(arr,'_unload_load_transform')||findJobContains(arr,'unload_load_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 in der Regel Unload und Update hintereinander aus.',searched:id};}return {available:false,job:null,sourceDatabase:null,endpoint:null,reason:reasonForMissing(m,err,arr,'kein kombinierter Unload-&-Update-Job gefunden. Gesucht wurde '+id+' beziehungsweise *_unload_load_transform.'),searched:id};}
function unloadPlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),id=abbr+'_unload';var j=findJobById(arr,id)||findJobBySuffix(arr,'_unload');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 nur den Unload aus.',searched:id};}return {available:false,job:null,sourceDatabase:null,endpoint:null,reason:reasonForMissing(m,err,arr,'kein einzelner Unload-Job gefunden. Gesucht wurde '+id+' beziehungsweise *_unload.'),searched:id};}
function updateOnlyPlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),id=abbr+'_load_and_transform';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};}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);var r=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(m.installed===false){r='Das Modul ist laut Komponentenliste nicht installiert. Der Upgrade-Endpunkt wird trotzdem angeboten, falls die serverseitige Logik daraus eine Installation oder Reparatur ableiten kann.';}if(err){r+=' Hinweis: Die technische Jobliste konnte nicht geladen werden: '+err;}else if(installUpgrade&&installUpgrade.id){r+=' In der technischen Jobliste wurde zusätzlich '+installUpgrade.id+' gefunden.';}else{r+=' In der technischen Jobliste wurde kein *_install_upgrade-Job gefunden; gestartet wird dennoch der Komponenten-Endpunkt.';}return {available:!!abbr,endpoint:'/job/upgrade/'+abbr,job:installUpgrade,reason:r,versionInfo:vi};}
function completePlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),id=abbr+'_unload_load_transform';if(m.installed!==true){return notInstalledPlan(m,id);}var j=findJobById(arr,id)||findJobBySuffix(arr,'_unload_load_transform')||findJobContains(arr,'unload_load_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 in der Regel Unload und Update hintereinander aus.',searched:id};}if(flagTrue(m.unload)&&flagTrue(m.load)){return inferredPlan(m,id,'Über /job/list/'+abbr+' wurde kein passender Kombi-Job gefunden. /etl/list meldet jedoch unload=true und 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 kombinierter Unload-&-Update-Job gefunden. Gesucht wurde '+id+' beziehungsweise *_unload_load_transform.'),searched:id};}
function unloadPlan(m){var abbr=key(m),arr=jobs(m),err=jobErr(m),id=abbr+'_unload';if(m.installed!==true){return notInstalledPlan(m,id);}var j=findJobById(arr,id)||findJobBySuffix(arr,'_unload');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 nur den Unload aus.',searched:id};}if(flagTrue(m.unload)){return inferredPlan(m,id,'Über /job/list/'+abbr+' wurde kein passender Unload-Job gefunden. /etl/list meldet jedoch unload=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 Unload-Job gefunden. Gesucht wurde '+id+' beziehungsweise *_unload.'),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 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);h+='<div class="box"><h3>Upgrade</h3><div class="plan-grid"><div class="muted">Bezeichnung</div><div>Modulaktualisierung</div><div class="muted">verfügbar</div><div>'+(ug.available?badge('ja','ok'):badge('nein','warn'))+'</div><div class="muted">Upgradebedarf</div><div>'+(m.needsUpgrade===true?badge('laut /etl/list ja','warn'):badge('laut /etl/list nein','neutral'))+'</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='';return fetchJson(API_BASE+'/list').then(function(data){if(!Array.isArray(data)){throw new Error('Unerwartete Antwort von /etl/list');}modules=data;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 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 loadAllJobs(){var ps=[];for(var i=0;i<modules.length;i++){ps.push(loadJobList(modules[i]));}return Promise.all(ps);}
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);});}
@ -722,19 +797,49 @@ function renderVersionDiagnostics(){if(!el.diagnosticsVersions){return;}if(!modu
@@ -722,19 +797,49 @@ function renderVersionDiagnostics(){if(!el.diagnosticsVersions){return;}if(!modu
function restHint(label,errorText){var t=String(errorText||'');if(t.indexOf('HTTP 500')>=0&&label.indexOf('/jobLog/')>=0){return 'Hinweis: Bei /jobLog/{id} war eine fehlende oder unvollständige Tabelle job_log eine bekannte Ursache. Prüfen Sie den Bereich „Datenbank / job_log“.';}if(t.indexOf('HTTP 500')>=0&&label.indexOf('/job/list/')>=0){return 'Hinweis: Bei /job/list/{modul} können fehlende Moduldateien, ein falscher aktiver Webapp-Kontext oder fehlende JVM-Optionen mögliche Ursachen sein.';}if(t.indexOf('HTTP 404')>=0){return 'Hinweis: 404 deutet häufig auf einen falschen Context-Pfad, falschen Port/Tunnel oder eine andere aktive Webapp hin.';}if(t.indexOf('Failed to fetch')>=0||t.indexOf('NetworkError')>=0){return 'Hinweis: Browser konnte den Endpunkt nicht erreichen. Tunnel, Port, Context-Pfad oder Anmeldung prüfen.';}return 'Hinweis: Fehlerdetails im Tomcat-Log prüfen. Beispiel-Kommandos stehen unten im Diagnosebereich.';}
function diagRow(label,state,detail,hint){var type=state==='ok'?'ok':(state==='error'?'error':(state==='skip'?'info':'warn'));return '<div class="diag-status-row"><div>'+code(label)+'</div><div>'+badge(state==='ok'?'ok':(state==='skip'?'übersprungen':(state==='error'?'Fehler':'Hinweis')),type)+'</div><div>'+esc(detail||'')+(hint?'<div class="diag-hint">'+esc(hint)+'</div>':'')+'</div></div>';}
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 startPlan(m,planFn,label){if(!m){return;}var p=planFn(m);if(!p.available){msg(label+' ist nicht verfügbar: '+p.reason,'error');return;}startJob(API_BASE+p.endpoint,label+' '+(m.name||key(m))+' ('+p.job.id+')');}
function startUpgrade(m){if(!m){return;}var p=upgradePlan(m);if(!p.available){msg('Upgrade ist nicht verfügbar: '+p.reason,'error');return;}var abbr=key(m);var text='Upgrade starten?\n\n'+(m.name||abbr)+'\n\nHinweis: '+(m.needsUpgrade===true?'Upgradebedarf erkannt.':'Aktuell wird kein Upgradebedarf gemeldet. Das Upgrade wird bewusst erneut ausgeführt.')+'\n\nInstallierte Version: '+(p.versionInfo.installed||'nicht geliefert')+'\nVerfügbare Version: '+(p.versionInfo.target||'nicht geliefert');if(!confirm(text)){return;}startJob(API_BASE+'/job/upgrade/'+encodeURIComponent(abbr),'Upgrade '+(m.name||abbr));}
function startJob(url,label){hideMsg();setLog('', 'Neuer Lauf wird gestartet.');appendLog('Starte: '+label);appendLog(url);fetchJson(url).then(function(result){var id=parseId(result);if(id===-1){msg('Der Server hat -1 zurückgeliefert. Es wurde kein neuer Job gestartet. Vermutlich läuft bereits ein anderer Job.','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;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');startPolling();}).catch(function(e){msg(e.message||String(e),'error');appendLog('Fehler beim Start: '+(e.message||String(e)));});}function parseId(r){if(typeof r==='number'){return r;}if(typeof r==='string'){var s=r.trim();if(/^-?\d+$/.test(s)){return parseInt(s,10);}try{return parseId(JSON.parse(s));}catch(e){return null;}}if(r&&typeof r==='object'){var keys=['jobExecutionId','id','executionId'];for(var i=0;i<keys.length;i++){if(r[keys[i]]!==undefined&&r[keys[i]]!==null){return parseId(r[keys[i]]);}}}return null;}
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 startJob(url,label){hideMsg();setLog('', 'Neuer Lauf wird gestartet.');appendLog('Starte: '+label);appendLog(url);fetchJson(url).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 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 logIdCandidates(){var ids=[];if(lastStatusPayload&&typeof lastStatusPayload==='object'){addUniqueId(ids,fieldId(lastStatusPayload,['jobInstanceId','job_instance_id','jobInstanceID','instanceId','jobLogId','job_log_id']));if(lastStatusPayload.jobInstance){addUniqueId(ids,fieldId(lastStatusPayload.jobInstance,['jobInstanceId','id']));}}addUniqueId(ids,currentLogId);addUniqueId(ids,currentExecutionId);return ids;}
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 stopPolling(){if(pollTimer){clearInterval(pollTimer);pollTimer=null;}}
function pollStatus(){if(!currentExecutionId){return Promise.resolve();}return fetchJson(API_BASE+'/jobExecutionStatus/'+encodeURIComponent(currentExecutionId)).then(function(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){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 statusOf(d){if(!d){return '';}if(typeof d==='string'){return d;}if(d.status){return String(d.status);}if(d.exitStatus&&d.exitStatus.exitCode){return String(d.exitStatus.exitCode);}if(d.batchStatus){return String(d.batchStatus);}return '';}
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 isUnknownStatus(s){var u=String(s||'').trim().toUpperCase();return u==='UNKNOWN'||u==='UNBEKANNT';}
function statusOf(d){
if(!d){return '';}
if(typeof d==='string'){return d;}
var primary=d.status!==undefined&&d.status!==null?String(d.status):'';
function doneStatus(s){s=String(s||'').toUpperCase();return s.indexOf('COMPLETED')>=0||s.indexOf('FAILED')>=0||s.indexOf('STOPPED')>=0||s.indexOf('ABANDONED')>=0;}
function renderStatus(data){var s=statusOf(data);el.runStatus.textContent=s||stringify(data).substring(0,300);el.runId.textContent=currentExecutionId||'-';el.runJob.textContent=currentJobLabel||'-';var p=progress(data);if(el.progressBar.tagName&&el.progressBar.tagName.toLowerCase()==='progress'){el.progressBar.value=p.percent;el.progressBar.textContent=p.percent+'%';}else{el.progressBar.style.width=p.percent+'%';}el.progressText.textContent=p.text;if(doneStatus(s)){el.stopRunBtn.disabled=true;}}function progress(d){var done=null,total=null;if(d&&typeof d==='object'){var dk=['executedStepsCount','completedStepsCount','executedSteps','stepsExecuted','writeCount'],tk=['totalStepsCount','countTotalSteps','totalSteps','stepCount'];for(var i=0;i<dk.length;i++){if(d[dk[i]]!==undefined&&d[dk[i]]!==null&&!isNaN(Number(d[dk[i]]))){done=Number(d[dk[i]]);break;}}for(var j=0;j<tk.length;j++){if(d[tk[j]]!==undefined&&d[tk[j]]!==null&&!isNaN(Number(d[tk[j]]))){total=Number(d[tk[j]]);break;}}if((done===null||total===null)&&Array.isArray(d.stepExecutions)){total=d.stepExecutions.length;done=0;for(var k=0;k<d.stepExecutions.length;k++){if(doneStatus(statusOf(d.stepExecutions[k]))){done++;}}}}if(total!==null&&total>0&&done!==null){var pc=Math.max(0,Math.min(100,Math.round(done*100/total)));return {percent:pc,text:done+' von '+total+' Schritten ('+pc+'%)'};}var s=statusOf(d).toUpperCase();if(s.indexOf('COMPLETED')>=0){return {percent:100,text:'abgeschlossen'};}if(s.indexOf('START')>=0||s.indexOf('RUNNING')>=0){return {percent:35,text:'läuft, Detailfortschritt nicht verfügbar'};}return {percent:0,text:'kein Fortschritt verfügbar'};}
function loadLog(userRequested){if(!currentExecutionId){return Promise.resolve();}var id=currentExecutionId;if(logFailureByExecutionId[id]&&userRequested!==true){return Promise.resolve();}el.logHint.textContent='Log wird geladen...';return fetchJson(API_BASE+'/jobLog/'+encodeURIComponent(id)).then(function(data){logFailureByExecutionId[id]=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){var text=e.message||String(e);if(userRequested!==true&&!logFailureByExecutionId[id]){logFailureByExecutionId[id]=true;el.logHint.textContent='Log noch nicht verfügbar. Automatischer zweiter Versuch folgt.';return new Promise(function(resolve){setTimeout(resolve,3000);}).then(function(){logFailureByExecutionId[id]=false;return loadLog(true);});}logFailureByExecutionId[id]=true;var hint='Log für JobExecutionId '+id+' ist über /jobLog/'+id+' nicht verfügbar.';el.logHint.textContent='Log nicht verfügbar.';if(userRequested===true){appendLog(hint);appendLog(text);appendLog('Hinweis: Prüfen Sie über Diagnose die REST-Schnittstelle /jobLog/{id} und die Tabelle job_log.');}else{appendLog(hint+' Der Laufstatus wurde trotzdem abgefragt; vermutlich liefert der Server für diesen Log-Endpunkt HTTP 500.');}return Promise.resolve();});}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;currentJobLabel=(data&&data.jobName)?data.jobName:'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){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;}if(!confirm('Aktuellen Job stoppen?\n\nJobExecutionId: '+currentExecutionId)){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 renderStatus(data){var s=statusOf(data);el.runStatus.textContent=displayRunStatus(s)||stringify(data).substring(0,300);el.runId.textContent=currentExecutionId||'-';el.runJob.textContent=currentJobLabel||'-';var p=progress(data);if(el.progressBar.tagName&&el.progressBar.tagName.toLowerCase()==='progress'){el.progressBar.value=p.percent;el.progressBar.textContent=p.percent+'%';}else{el.progressBar.style.width=p.percent+'%';}el.progressText.textContent=p.text;if(doneStatus(s)){el.stopRunBtn.disabled=true;}}function progress(d){var done=null,total=null;if(d&&typeof d==='object'){var dk=['executedStepsCount','completedStepsCount','executedSteps','stepsExecuted','writeCount'],tk=['totalStepsCount','countTotalSteps','totalSteps','stepCount'];for(var i=0;i<dk.length;i++){if(d[dk[i]]!==undefined&&d[dk[i]]!==null&&!isNaN(Number(d[dk[i]]))){done=Number(d[dk[i]]);break;}}for(var j=0;j<tk.length;j++){if(d[tk[j]]!==undefined&&d[tk[j]]!==null&&!isNaN(Number(d[tk[j]]))){total=Number(d[tk[j]]);break;}}if((done===null||total===null)&&Array.isArray(d.stepExecutions)){total=d.stepExecutions.length;done=0;for(var k=0;k<d.stepExecutions.length;k++){if(doneStatus(statusOf(d.stepExecutions[k]))){done++;}}}}if(total!==null&&total>0&&done!==null){var pc=Math.max(0,Math.min(100,Math.round(done*100/total)));return {percent:pc,text:done+' von '+total+' Schritten ('+pc+'%)'};}var s=statusOf(d).toUpperCase();if(s.indexOf('COMPLETED')>=0){return {percent:100,text:'abgeschlossen'};}if(isUnknownStatus(s)||s.indexOf('START')>=0||s.indexOf('RUNNING')>=0){return {percent:35,text:'läuft, Detailfortschritt noch nicht verfügbar'};}return {percent:0,text:'kein Fortschritt verfügbar'};}
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,[]);}
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');});});}