';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='
';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='
Module konnten nicht geladen werden.
';msg(e.message||String(e),'error');});}
function loadAllJobs(){var ps=[];for(var i=0;i';el.diagnosticsVersions.innerHTML=h;}
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 '
';var checks=[];checks.push({label:'/etl/list',url:API_BASE+'/list'});checks.push({label:'/etl/installables/list',url:API_BASE+'/installables/list'});checks.push({label:'/running',url:API_BASE+'/running'});for(var i=0;i=checks.length){el.diagnosticsRest.innerHTML=results.join('');return Promise.resolve();}var c=checks[idx];if(c.skip){results.push(diagRow(c.label,'skip',c.detail,''));el.diagnosticsRest.innerHTML=results.join('');return next(idx+1);}return fetchText(c.url).then(function(t){var detail='erreichbar';if(t&&t.length){detail+='; Antwortlänge '+t.length+' Zeichen';}results.push(diagRow(c.label,'ok',detail,''));el.diagnosticsRest.innerHTML=results.join('');}).catch(function(e){var text=e.message||String(e);results.push(diagRow(c.label,'error',text,restHint(c.label,text)));el.diagnosticsRest.innerHTML=results.join('');}).then(function(){return next(idx+1);});}return next(0);}
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;i0&&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 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;i0)?id:null;}
+function fieldId(o,keys){if(!o||typeof o!=='object'){return null;}for(var i=0;i=0){return 'Abgeschlossen';}if(u.indexOf('FAILED')>=0){return 'Fehlgeschlagen';}if(u.indexOf('STOPPED')>=0){return 'Gestoppt';}if(u.indexOf('ABANDONED')>=0){return 'Abgebrochen';}if(isUnknownStatus(raw)){return 'Läuft';}if(u.indexOf('RUNNING')>=0||u.indexOf('STARTED')>=0||u.indexOf('STARTING')>=0||u.indexOf('ANLAUF')>=0||u.indexOf('ANLAUEN')>=0){return 'Läuft';}return raw;}
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;i0&&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;i0&&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='