SuperX-Kernmodul
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

744 lines
74 KiB

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.io.File" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.lang.management.ManagementFactory" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.sql.SQLException" %>
<%@ page import="java.sql.Statement" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.Date" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.LinkedHashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="javax.naming.InitialContext" %>
<%@ page import="javax.sql.DataSource" %>
<%@ page import="jakarta.servlet.ServletContext" %>
<%@ page import="javax.xml.parsers.DocumentBuilder" %>
<%@ page import="javax.xml.parsers.DocumentBuilderFactory" %>
<%@ page import="org.w3c.dom.Document" %>
<%!
private String html(Object value) {
if (value == null) return "";
String s = String.valueOf(value);
return s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace(String.valueOf((char)34), "&quot;")
.replace("'", "&#39;");
}
private String tag(String text, String type) {
String cls = "is-light";
if ("ok".equals(type)) cls = "is-success is-light";
else if ("warn".equals(type)) cls = "is-warning is-light";
else if ("error".equals(type)) cls = "is-danger is-light";
else if ("info".equals(type)) cls = "is-info is-light";
return "<span class='tag " + cls + "'>" + html(text) + "</span>";
}
private String formatBytes(long bytes) {
if (bytes < 0) return "-";
String[] units = {"B", "KB", "MB", "GB", "TB"};
double value = (double) bytes;
int idx = 0;
while (value >= 1024.0 && idx < units.length - 1) {
value = value / 1024.0;
idx++;
}
if (idx == 0) return String.valueOf(bytes) + " " + units[idx];
return String.format(java.util.Locale.GERMANY, "%.1f %s", value, units[idx]);
}
private String nullText(Object value) {
if (value == null || String.valueOf(value).length() == 0) {
return "<span class='muted'>nicht sichtbar / nicht gesetzt</span>";
}
return html(value);
}
private String jsString(Object value) {
if (value == null) return "";
String s = String.valueOf(value);
return s.replace("\\", "\\\\")
.replace("\r", "\\r")
.replace("\n", "\\n")
.replace("\t", "\\t")
.replace("\"", "\\\"");
}
private String joinPath(String base, String child) {
if (base == null || base.length() == 0) return null;
return new File(base, child).getPath();
}
private String fileCheckRow(String label, String path) {
StringBuilder sb = new StringBuilder();
sb.append("<tr><td>").append(html(label)).append("</td><td>");
if (path == null || path.length() == 0) {
sb.append("<span class='muted'>nicht ermittelbar</span>");
sb.append("</td><td>").append(tag("unbekannt", "warn")).append("</td><td></td></tr>");
return sb.toString();
}
File f = new File(path);
sb.append("<span class='code'>").append(html(path)).append("</span>");
sb.append("</td><td>");
sb.append(f.exists() ? tag("vorhanden", "ok") : tag("fehlt", "warn"));
sb.append("</td><td>");
if (f.exists()) {
sb.append("lesbar: ").append(f.canRead() ? "ja" : "nein");
sb.append(", schreibbar: ").append(f.canWrite() ? "ja" : "nein");
if (f.isDirectory()) sb.append(", betretbar: ").append(f.canExecute() ? "ja" : "nein");
}
sb.append("</td></tr>");
return sb.toString();
}
private boolean tableExistsByRegclass(Connection con, String regclassName) {
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = con.prepareStatement("select to_regclass(?) is not null");
ps.setString(1, regclassName);
rs = ps.executeQuery();
return rs.next() && rs.getBoolean(1);
} catch (Exception e) {
return false;
} finally {
try { if (rs != null) rs.close(); } catch (Exception e) { }
try { if (ps != null) ps.close(); } catch (Exception e) { }
}
}
private boolean hasDbVersionTable(Connection con) {
return tableExistsByRegclass(con, "db_version") || tableExistsByRegclass(con, "public.db_version");
}
private Connection connectionFromObject(Object obj, boolean requireDbVersion) throws Exception {
if (!(obj instanceof DataSource)) return null;
Connection con = null;
try {
con = ((DataSource) obj).getConnection();
if (!requireDbVersion || hasDbVersionTable(con)) return con;
} catch (Exception e) {
try { if (con != null) con.close(); } catch (Exception ex) { }
throw e;
}
try { if (con != null) con.close(); } catch (Exception e) { }
return null;
}
private Object springBean(Object wac, String beanName) {
try {
java.lang.reflect.Method getBean = wac.getClass().getMethod("getBean", String.class);
return getBean.invoke(wac, beanName);
} catch (Exception e) {
return null;
}
}
private Connection connectionFromMap(Object obj, boolean requireDbVersion) throws Exception {
if (!(obj instanceof Map)) return null;
Map map = (Map) obj;
String[] preferredKeys = {"superx", "SuperX", "default", "hisinone", "eduetl"};
for (int i = 0; i < preferredKeys.length; i++) {
if (map.containsKey(preferredKeys[i])) {
Connection c = connectionFromObject(map.get(preferredKeys[i]), requireDbVersion);
if (c != null) return c;
}
}
for (Object value : map.values()) {
Connection c = connectionFromObject(value, requireDbVersion);
if (c != null) return c;
}
return null;
}
private Connection findSpringConnection(ServletContext application, boolean requireDbVersion) throws Exception {
Object wac = null;
try {
Class util = Class.forName("org.springframework.web.context.support.WebApplicationContextUtils");
java.lang.reflect.Method m = util.getMethod("getWebApplicationContext", ServletContext.class);
wac = m.invoke(null, application);
} catch (Exception e) {
wac = null;
}
if (wac == null) return null;
String[] beanNames = {"dataSources", "superxDataSource", "dataSource", "eduetlDataSource", "hisinoneDataSource"};
for (int i = 0; i < beanNames.length; i++) {
Object bean = springBean(wac, beanNames[i]);
Connection c = connectionFromMap(bean, requireDbVersion);
if (c != null) return c;
c = connectionFromObject(bean, requireDbVersion);
if (c != null) return c;
}
try {
java.lang.reflect.Method namesMethod = wac.getClass().getMethod("getBeanNamesForType", Class.class);
String[] names = (String[]) namesMethod.invoke(wac, DataSource.class);
if (names != null) {
for (int i = 0; i < names.length; i++) {
Object bean = springBean(wac, names[i]);
Connection c = connectionFromObject(bean, requireDbVersion);
if (c != null) return c;
}
}
} catch (Exception e) { }
return null;
}
private Connection findJndiConnection(boolean requireDbVersion) throws Exception {
String[] jndiNames = {
"java:comp/env/jdbc/superx",
"java:comp/env/jdbc/eduetl",
"java:comp/env/jdbc/hisinone",
"jdbc/superx",
"jdbc/eduetl",
"jdbc/hisinone"
};
InitialContext ctx = null;
try { ctx = new InitialContext(); } catch (Exception e) { ctx = null; }
if (ctx == null) return null;
for (int i = 0; i < jndiNames.length; i++) {
try {
Object obj = ctx.lookup(jndiNames[i]);
Connection c = connectionFromObject(obj, requireDbVersion);
if (c != null) return c;
} catch (Exception e) { }
}
return null;
}
private Connection findServletContextConnection(ServletContext application, boolean requireDbVersion) throws Exception {
String[] attrNames = {"dataSources", "dataSource", "datasource", "superxDataSource", "eduetlDataSource", "javax.sql.DataSource"};
for (int i = 0; i < attrNames.length; i++) {
try {
Object obj = application.getAttribute(attrNames[i]);
Connection c = connectionFromMap(obj, requireDbVersion);
if (c != null) return c;
c = connectionFromObject(obj, requireDbVersion);
if (c != null) return c;
} catch (Exception e) { }
}
return null;
}
private Connection openDiagnosticConnection(ServletContext application) throws Exception {
// Erste Runde: bevorzugt eine Verbindung, in der db_version sichtbar ist.
Connection c = findSpringConnection(application, true);
if (c != null) return c;
c = findJndiConnection(true);
if (c != null) return c;
c = findServletContextConnection(application, true);
if (c != null) return c;
// Zweite Runde: irgendeine verfügbare Verbindung, damit Diagnose nicht komplett ausfällt.
c = findSpringConnection(application, false);
if (c != null) return c;
c = findJndiConnection(false);
if (c != null) return c;
c = findServletContextConnection(application, false);
if (c != null) return c;
throw new SQLException("Keine DataSource über Spring, JNDI oder ServletContext gefunden. REST-Checks und Dateisystem-Checks bleiben trotzdem verfügbar.");
}
private String sqlColumnRows(Connection con, String tableSchema, String tableName) throws Exception {
PreparedStatement ps = null;
ResultSet rs = null;
StringBuilder sb = new StringBuilder();
try {
ps = con.prepareStatement("select column_name, data_type from information_schema.columns where table_schema = ? and table_name = ? order by ordinal_position");
ps.setString(1, tableSchema);
ps.setString(2, tableName);
rs = ps.executeQuery();
while (rs.next()) {
sb.append("<tr><td>").append(html(rs.getString(1))).append("</td><td>").append(html(rs.getString(2))).append("</td></tr>");
}
} finally {
try { if (rs != null) rs.close(); } catch (Exception e) { }
try { if (ps != null) ps.close(); } catch (Exception e) { }
}
return sb.toString();
}
private String readTrimmedFile(File f) {
if (f == null || !f.exists() || !f.isFile() || !f.canRead()) return "";
StringBuilder sb = new StringBuilder();
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(f), "UTF-8"));
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
} catch (Exception e) {
return "";
} finally {
try { if (br != null) br.close(); } catch (Exception e) { }
}
return sb.toString().trim();
}
private void disableXmlFeature(DocumentBuilderFactory factory, String feature) {
try { factory.setFeature(feature, false); } catch (Exception e) { }
}
private String readModuleXmlVersion(File xmlFile) {
if (xmlFile == null || !xmlFile.exists() || !xmlFile.isFile() || !xmlFile.canRead()) return "";
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(false);
disableXmlFeature(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd");
disableXmlFeature(factory, "http://xml.org/sax/features/external-general-entities");
disableXmlFeature(factory, "http://xml.org/sax/features/external-parameter-entities");
try { factory.setXIncludeAware(false); } catch (Exception e) { }
try { factory.setExpandEntityReferences(false); } catch (Exception e) { }
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(xmlFile);
if (doc != null && doc.getDocumentElement() != null) {
String version = doc.getDocumentElement().getAttribute("version");
if (version != null) return version.trim();
}
} catch (Exception e) {
return "";
}
return "";
}
private void addAvailableVersionInfo(Map map, String abbr, String xmlPath, String versionPath) {
if (abbr == null || abbr.trim().length() == 0) return;
String key = abbr.trim().toLowerCase();
Map info = new LinkedHashMap();
File xmlFile = xmlPath == null ? null : new File(xmlPath);
File versionFile = versionPath == null ? null : new File(versionPath);
String xmlVersion = readModuleXmlVersion(xmlFile);
String fileVersion = readTrimmedFile(versionFile);
info.put("xmlPath", xmlPath == null ? "" : xmlPath);
info.put("xmlVersion", xmlVersion == null ? "" : xmlVersion);
info.put("versionFilePath", versionPath == null ? "" : versionPath);
info.put("versionFileVersion", fileVersion == null ? "" : fileVersion);
if (xmlVersion != null && xmlVersion.length() > 0) {
info.put("version", xmlVersion);
info.put("source", "module_xml");
} else if (fileVersion != null && fileVersion.length() > 0) {
info.put("version", fileVersion);
info.put("source", "version_file");
} else {
info.put("version", "");
info.put("source", "");
}
map.put(key, info);
}
private String availableVersionJson(Map map) {
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (Object entryObj : map.entrySet()) {
Map.Entry entry = (Map.Entry) entryObj;
if (!first) sb.append(",");
first = false;
String key = String.valueOf(entry.getKey());
Map info = (Map) entry.getValue();
sb.append("\"").append(jsString(key)).append("\":{");
String[] props = {"version", "source", "xmlVersion", "xmlPath", "versionFileVersion", "versionFilePath"};
for (int i = 0; i < props.length; i++) {
if (i > 0) sb.append(",");
Object val = info.get(props[i]);
sb.append("\"").append(props[i]).append("\":\"").append(jsString(val == null ? "" : String.valueOf(val))).append("\"");
}
sb.append("}");
}
sb.append("}");
return sb.toString();
}
private String availableVersionRows(Map map) {
StringBuilder sb = new StringBuilder();
for (Object entryObj : map.entrySet()) {
Map.Entry entry = (Map.Entry) entryObj;
String key = String.valueOf(entry.getKey());
Map info = (Map) entry.getValue();
sb.append("<tr><td>").append(html(key)).append("</td>");
sb.append("<td>").append(info.get("xmlVersion") == null || String.valueOf(info.get("xmlVersion")).length() == 0 ? "<span class='muted'>nicht gefunden</span>" : html(info.get("xmlVersion"))).append("</td>");
sb.append("<td><span class='code'>").append(html(info.get("xmlPath"))).append("</span></td>");
sb.append("<td>").append(info.get("versionFileVersion") == null || String.valueOf(info.get("versionFileVersion")).length() == 0 ? "<span class='muted'>nicht gefunden</span>" : html(info.get("versionFileVersion"))).append("</td>");
sb.append("<td><span class='code'>").append(html(info.get("versionFilePath"))).append("</span></td>");
sb.append("<td>").append(info.get("version") == null || String.valueOf(info.get("version")).length() == 0 ? "<span class='muted'>nicht geliefert</span>" : html(info.get("version"))).append("</td></tr>");
}
return sb.toString();
}
%>
<%
String diagGeneratedAt = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(new Date());
String contextPath = request.getContextPath();
String servletRealPath = application.getRealPath("/");
String canonicalRealPath = "";
try { canonicalRealPath = servletRealPath == null ? "" : new File(servletRealPath).getCanonicalPath(); }
catch (Exception e) { canonicalRealPath = "nicht ermittelbar: " + e.getMessage(); }
String webInfPath = joinPath(servletRealPath, "WEB-INF");
String edustorePath = joinPath(webInfPath, "conf/edustore");
String dbPath = joinPath(edustorePath, "db");
String modulePath = joinPath(dbPath, "module");
String kernInstallPath = joinPath(dbPath, "install");
String kernXmlPath = joinPath(kernInstallPath, "conf/kern.xml");
String kernVersionFilePath = joinPath(kernInstallPath, "VERSION");
String kennPath = joinPath(modulePath, "kenn");
String kennXmlPath = joinPath(kennPath, "conf/kenn.xml");
String kennRohdatenPath = joinPath(kennPath, "rohdaten");
String kennSchluesselPath = joinPath(kennPath, "schluesseltabellen");
File diskBase = servletRealPath == null ? null : new File(servletRealPath);
long diskTotal = diskBase == null ? -1 : diskBase.getTotalSpace();
long diskFree = diskBase == null ? -1 : diskBase.getFreeSpace();
long diskUsable = diskBase == null ? -1 : diskBase.getUsableSpace();
boolean diskLow = diskUsable >= 0 && diskUsable < 1024L * 1024L * 1024L;
List jvmArgsList = ManagementFactory.getRuntimeMXBean().getInputArguments();
StringBuilder jvmArgsBuilder = new StringBuilder();
for (int i = 0; i < jvmArgsList.size(); i++) {
if (i > 0) jvmArgsBuilder.append(System.lineSeparator());
jvmArgsBuilder.append(String.valueOf(jvmArgsList.get(i)));
}
String jvmArgs = jvmArgsBuilder.toString();
boolean addOpensVisible = jvmArgs.indexOf("--add-opens=java.base/sun.net.www.protocol.jar=ALL-UNNAMED") >= 0;
String dbDiagnosticMessage = "";
String dbInfoHtml = "";
String jobLogTableTag = tag("unbekannt", "warn");
String jobLogColumnsHtml = "";
boolean dbDirectAvailable = false;
boolean jobLogExists = false;
boolean jobLogHasJobInstanceId = false;
boolean jobLogHasLog = false;
boolean jobLogHasStatus = false;
String dbVersionTableTag = tag("unbekannt", "warn");
String dbVersionRowsHtml = "";
String dbVersionFallbackJson = "{}";
String availableVersionFallbackJson = "{}";
String availableVersionRowsHtml = "";
Map availableVersionMap = new LinkedHashMap();
addAvailableVersionInfo(availableVersionMap, "kern", kernXmlPath, kernVersionFilePath);
if (modulePath != null) {
File moduleDir = new File(modulePath);
File[] moduleDirs = moduleDir.exists() && moduleDir.isDirectory() ? moduleDir.listFiles() : null;
if (moduleDirs != null) {
for (int i = 0; i < moduleDirs.length; i++) {
File md = moduleDirs[i];
if (md != null && md.isDirectory()) {
String abbr = md.getName();
if (!"kern".equalsIgnoreCase(abbr)) {
addAvailableVersionInfo(availableVersionMap, abbr, joinPath(md.getPath(), "conf/" + abbr + ".xml"), joinPath(md.getPath(), "VERSION"));
}
}
}
}
}
availableVersionFallbackJson = availableVersionJson(availableVersionMap);
availableVersionRowsHtml = availableVersionRows(availableVersionMap);
boolean dbVersionExists = false;
Connection diagCon = null;
try {
diagCon = openDiagnosticConnection(application);
dbDirectAvailable = true;
Statement st = null;
ResultSet rs = null;
try {
st = diagCon.createStatement();
rs = st.executeQuery("select current_database(), current_schema(), version()");
if (rs.next()) {
dbInfoHtml = "<tr><td>Datenbank</td><td>" + html(rs.getString(1)) + "</td></tr>" +
"<tr><td>Schema</td><td>" + html(rs.getString(2)) + "</td></tr>" +
"<tr><td>DB-Version</td><td>" + html(rs.getString(3)) + "</td></tr>";
}
} catch (Exception e) {
dbInfoHtml = "<tr><td>Direkte DB-Information</td><td>" + tag("nicht verfügbar", "warn") + " " + html(e.getMessage()) + "</td></tr>";
} finally {
try { if (rs != null) rs.close(); } catch (Exception e) { }
try { if (st != null) st.close(); } catch (Exception e) { }
}
PreparedStatement ps = null;
rs = null;
try {
ps = diagCon.prepareStatement("select to_regclass('public.job_log') is not null");
rs = ps.executeQuery();
if (rs.next()) jobLogExists = rs.getBoolean(1);
jobLogTableTag = jobLogExists ? tag("vorhanden", "ok") : tag("fehlt", "warn");
} catch (Exception e) {
jobLogTableTag = tag("Prüfung fehlgeschlagen", "warn") + " " + html(e.getMessage());
} finally {
try { if (rs != null) rs.close(); } catch (Exception e) { }
try { if (ps != null) ps.close(); } catch (Exception e) { }
}
if (jobLogExists) {
jobLogColumnsHtml = sqlColumnRows(diagCon, "public", "job_log");
ps = null;
rs = null;
try {
ps = diagCon.prepareStatement("select column_name from information_schema.columns where table_schema = 'public' and table_name = 'job_log'");
rs = ps.executeQuery();
while (rs.next()) {
String c = rs.getString(1);
if ("job_instance_id".equalsIgnoreCase(c)) jobLogHasJobInstanceId = true;
if ("log".equalsIgnoreCase(c)) jobLogHasLog = true;
if ("status".equalsIgnoreCase(c)) jobLogHasStatus = true;
}
} finally {
try { if (rs != null) rs.close(); } catch (Exception e) { }
try { if (ps != null) ps.close(); } catch (Exception e) { }
}
}
ps = null;
rs = null;
try {
ps = diagCon.prepareStatement("select (to_regclass('db_version') is not null or to_regclass('public.db_version') is not null)");
rs = ps.executeQuery();
if (rs.next()) dbVersionExists = rs.getBoolean(1);
dbVersionTableTag = dbVersionExists ? tag("vorhanden", "ok") : tag("fehlt", "warn");
} catch (Exception e) {
dbVersionTableTag = tag("Prüfung fehlgeschlagen", "warn") + " " + html(e.getMessage());
} finally {
try { if (rs != null) rs.close(); } catch (Exception e) { }
try { if (ps != null) ps.close(); } catch (Exception e) { }
}
if (dbVersionExists) {
ps = null;
rs = null;
StringBuilder versionJson = new StringBuilder();
boolean firstVersion = true;
versionJson.append("{");
try {
ps = diagCon.prepareStatement("select trim(his_system) as his_system, trim(version) as version, systeminfo_id from db_version order by his_system, systeminfo_id");
rs = ps.executeQuery();
while (rs.next()) {
String sys = rs.getString(1);
String ver = rs.getString(2);
Integer sid = null;
try { sid = (Integer) rs.getObject(3); } catch (Exception e) { sid = null; }
if (sys != null && ver != null && sys.trim().length() > 0 && ver.trim().length() > 0) {
String key = sys.trim().toLowerCase(java.util.Locale.ROOT);
if (!firstVersion) versionJson.append(",");
versionJson.append("\"").append(jsString(key)).append("\":\"").append(jsString(ver.trim())).append("\"");
firstVersion = false;
if (sid != null) {
versionJson.append(",\"sid:").append(sid.intValue()).append("\":\"").append(jsString(ver.trim())).append("\"");
}
dbVersionRowsHtml += "<tr><td>" + html(sys.trim()) + "</td><td>" + html(ver.trim()) + "</td><td>" + html(sid == null ? "" : sid) + "</td></tr>";
}
}
} catch (Exception e) {
dbVersionRowsHtml = "<tr><td colspan='3'>" + tag("Prüfung fehlgeschlagen", "warn") + " " + html(e.getMessage()) + "</td></tr>";
} finally {
try { if (rs != null) rs.close(); } catch (Exception e) { }
try { if (ps != null) ps.close(); } catch (Exception e) { }
}
versionJson.append("}");
dbVersionFallbackJson = versionJson.toString();
}
} catch (Exception e) {
dbDiagnosticMessage = e.getMessage();
dbInfoHtml = "<tr><td>Direkte DB-Prüfung</td><td>" + tag("nicht verfügbar", "warn") + " " + html(e.getMessage()) + "</td></tr>";
} finally {
try { if (diagCon != null) diagCon.close(); } catch (Exception e) { }
}
%>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>SuperX ETL Manager</title>
<link rel="stylesheet" href="/superx/style/bulma.css">
<style>
body{background:#f5f7fa}
.page{max-width:1550px;margin:0 auto;padding:1.5rem}
.header{background:linear-gradient(135deg,#1f5f8b,#164866);color:#fff;border-radius:12px;padding:1.5rem;margin-bottom:1rem;box-shadow:0 8px 18px rgba(31,95,139,.18)}
.header h1{color:#fff;margin:0 0 .5rem 0;font-size:1.5rem;font-weight:700}
.header p{color:#fff;margin:0;line-height:1.45;opacity:.93}
.toolbar{background:#fff;border-radius:8px;padding:1rem;margin-bottom:1rem;display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.02)}
.toolbar input{min-width:280px;flex:1}
.spacer{flex:1}
.hidden{display:none!important}
.module-name{font-weight:700;font-size:.95rem;margin-bottom:.6rem}
.module-line{font-size:.8rem;color:#697386;margin-bottom:.45rem}
.version-line{font-size:.8rem;color:#697386;margin-bottom:.4rem}
.code{font-family:Consolas,Monaco,"Courier New",monospace;font-size:.78rem;background:#eef2f6;border:1px solid #dfe5ec;border-radius:5px;padding:.1rem .35rem;color:#273647;word-break:break-all}
.action-cell{min-width:470px}
.action-row{display:flex;flex-wrap:wrap;gap:.4rem}
.panel-head{display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin-bottom:1rem}
.panel-head h2{margin:0;font-size:1.15rem;font-weight:700}
.panel-body{padding:0}
.details-grid{display:grid;grid-template-columns:minmax(280px,1fr) minmax(520px,2fr);gap:1rem}
.kv{display:grid;grid-template-columns:160px 1fr;gap:.45rem .75rem}
.kv .k{color:#697386}
.box h3{margin:0 0 .75rem 0;font-size:.95rem;font-weight:700}
details.box summary{cursor:pointer;font-weight:700}
details.box summary:hover{color:#164866}
.plan-grid{display:grid;grid-template-columns:145px 1fr;gap:.5rem .75rem;align-items:start}
.run-grid{display:grid;grid-template-columns:220px 1fr;gap:.6rem .9rem;margin-bottom:.75rem}
.progress-wrap{width:100%;height:1rem;border-radius:999px;background:#e8edf3;overflow:hidden;border:1px solid #d8e0e8}
.progress-bar{height:100%;width:0;background:linear-gradient(90deg,#3c88b8,#65a9d2);transition:width .25s ease}
.log-actions{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.75rem}
pre.log{margin:0;min-height:340px;max-height:560px;overflow:auto;white-space:pre-wrap;word-break:break-word;padding:1rem;background:#111827;color:#e5e7eb;border-radius:8px;font-size:.78rem;line-height:1.45}
pre.json{white-space:pre-wrap;word-break:break-word;background:#f2f5f8;border:1px solid #dfe5ec;border-radius:8px;padding:.75rem;margin:.75rem 0 0 0;max-height:430px;overflow:auto;font-size:.8rem}
.message{margin-bottom:1rem}
.small,.muted{color:#697386}.small{font-size:.8rem;line-height:1.35}
.table td,.table th{vertical-align:top}
@media(max-width:1150px){.details-grid,.kv,.plan-grid,.run-grid{grid-template-columns:1fr}.action-cell{min-width:320px}}
.diagnostics-panel{border-left:5px solid #3c88b8}
.diagnostics-panel .panel-head{margin-bottom:.75rem}
.diagnostics-section{margin-top:1rem}
.diagnostics-section details{margin-bottom:.75rem}
.diagnostics-table td:first-child,.diagnostics-table th:first-child{width:260px;color:#697386}
.diagnostics-table td,.diagnostics-table th{font-size:.84rem}
.diag-status-list{display:grid;gap:.45rem}
.diag-status-row{display:grid;grid-template-columns:210px 120px 1fr;gap:.6rem;align-items:start;border-bottom:1px solid #eef2f6;padding:.45rem 0;font-size:.84rem}
.diag-status-row:last-child{border-bottom:0}
.diag-hint{color:#697386;font-size:.8rem;line-height:1.35;margin-top:.25rem}
.diag-textarea{width:100%;min-height:120px;font-family:Consolas,Monaco,"Courier New",monospace;font-size:.8rem;background:#111827;color:#e5e7eb;border:0;border-radius:8px;padding:.75rem;white-space:pre}
.diag-actions{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin:.5rem 0}
</style>
</head>
<body>
<div class="page">
<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 id="message" class="message hidden"></div>
<div class="toolbar"><input id="searchInput" type="search" placeholder="Module suchen nach Name, Kürzel, Systeminfo-ID oder Datenquelle"><select id="filterSelect"><option value="all">Alle Module</option><option value="installed">Installiert</option><option value="complete">Unload &amp; Update möglich</option><option value="unload">Unload möglich</option><option value="update">Update möglich</option><option value="dbok">DB verbunden</option><option value="dberror">DB nicht verbunden</option></select><button id="reloadBtn" class="button is-primary" type="button">Aktualisieren</button><span id="moduleCount" class="small muted"></span><span class="spacer"></span><button id="diagnosticsBtn" class="button is-info is-light" type="button">Diagnose</button></div>
<div id="diagnosticsPanel" class="box diagnostics-panel hidden">
<div class="panel-head"><h2>Systemdiagnose</h2><div><button id="refreshDiagnosticsBtn" class="button is-light" type="button">Diagnose aktualisieren</button> <button id="closeDiagnosticsBtn" class="button is-light" type="button">Schließen</button></div></div>
<div class="small muted">Stand: <%= html(diagGeneratedAt) %>. Die Diagnose ändert nichts an Dateien oder Datenbankobjekten. SQL- und Shell-Befehle werden nur zum Kopieren angezeigt.</div>
<div class="diagnostics-section">
<details class="box" open><summary>REST-Schnittstellen und aktuelle Oberfläche</summary><div id="diagnosticsRest" class="diag-status-list" style="margin-top:.75rem;"><div class="muted">Noch nicht geprüft. Bitte Diagnose aktualisieren.</div></div></details>
<details class="box" open><summary>Versionen</summary><div id="diagnosticsVersions" style="margin-top:.75rem;"><div class="muted">Noch nicht geprüft. Bitte Diagnose aktualisieren.</div></div><details style="margin-top:.75rem;"><summary>Verfügbare Versionen aus Modul-XML / VERSION-Dateien anzeigen</summary><table class="table is-fullwidth is-striped diagnostics-table"><thead><tr><th>Modul</th><th>Modul-XML</th><th>XML-Pfad</th><th>VERSION-Datei</th><th>VERSION-Pfad</th><th>verwendbarer Fallback</th></tr></thead><tbody><%= availableVersionRowsHtml %></tbody></table></details><div class="small muted">Installierte Version: REST/JSON, sonst db_version. Verfügbare Version: REST/JSON, sonst version-Attribut der Modul-XML, sonst VERSION-Datei. Für kern wird <span class="code">db/install/conf/kern.xml</span> verwendet.</div></details>
<details class="box" open><summary>Webapp / Tomcat / Java</summary><table class="table is-fullwidth diagnostics-table"><tbody>
<tr><td>Context Path</td><td><span class="code"><%= html(contextPath) %></span></td></tr>
<tr><td>Servlet Real Path</td><td><span class="code"><%= html(servletRealPath) %></span></td></tr>
<tr><td>Canonical Path</td><td><span class="code"><%= html(canonicalRealPath) %></span></td></tr>
<tr><td>Server Info</td><td><%= html(application.getServerInfo()) %></td></tr>
<tr><td>Java Version</td><td><%= html(System.getProperty("java.version")) %> / <%= html(System.getProperty("java.vendor")) %></td></tr>
<tr><td>OS</td><td><%= html(System.getProperty("os.name")) %> <%= html(System.getProperty("os.version")) %> (<%= html(System.getProperty("os.arch")) %>)</td></tr>
<tr><td>user.name</td><td><%= html(System.getProperty("user.name")) %></td></tr>
<tr><td>user.dir</td><td><span class="code"><%= html(System.getProperty("user.dir")) %></span></td></tr>
<tr><td>catalina.base</td><td><span class="code"><%= html(System.getProperty("catalina.base")) %></span></td></tr>
<tr><td>catalina.home</td><td><span class="code"><%= html(System.getProperty("catalina.home")) %></span></td></tr>
<tr><td>JAVA_OPTS</td><td><%= nullText(System.getenv("JAVA_OPTS")) %></td></tr>
<tr><td>CATALINA_OPTS</td><td><%= nullText(System.getenv("CATALINA_OPTS")) %></td></tr>
<tr><td>JVM Input Arguments</td><td><pre class="json"><%= html(jvmArgs) %></pre></td></tr>
<tr><td>Kettle/JarFileFactory add-opens</td><td><%= addOpensVisible ? tag("in JVM-Argumenten sichtbar", "ok") : tag("nicht sichtbar", "info") %><div class="diag-hint">Nur Hinweis: relevant, wenn beim Laden/Ausführen der Jobs eine InaccessibleObjectException zu sun.net.www.protocol.jar auftritt.</div></td></tr>
</tbody></table></details>
<details class="box" open><summary>Pfade und Dateisystem</summary><table class="table is-fullwidth diagnostics-table"><thead><tr><th>Prüfung</th><th>Pfad</th><th>Status</th><th>Rechte</th></tr></thead><tbody>
<%= fileCheckRow("WEB-INF", webInfPath) %><%= fileCheckRow("edustore", edustorePath) %><%= fileCheckRow("db", dbPath) %><%= fileCheckRow("db/install (Kernmodul)", kernInstallPath) %><%= fileCheckRow("db/install/conf/kern.xml", kernXmlPath) %><%= fileCheckRow("db/install/VERSION", kernVersionFilePath) %><%= fileCheckRow("module", modulePath) %><%= fileCheckRow("module/kenn", kennPath) %><%= fileCheckRow("kenn/conf/kenn.xml", kennXmlPath) %><%= fileCheckRow("kenn/rohdaten", kennRohdatenPath) %><%= fileCheckRow("kenn/schluesseltabellen", kennSchluesselPath) %>
</tbody></table><div class="small muted">Pfade werden nur angezeigt bzw. auf Existenz/Rechte geprüft. Aus dem Pfadnamen wird nicht automatisch abgeleitet, ob die Installation korrekt oder falsch ist. Besonderheit: Das Kernmodul wird unter <span class="code">db/install</span> geprüft, nicht unter <span class="code">db/module/kern</span>.</div></details>
<details class="box" open><summary>Festplattenspeicher</summary><table class="table is-fullwidth diagnostics-table"><tbody><tr><td>Prüfpfad</td><td><span class="code"><%= html(servletRealPath) %></span></td></tr><tr><td>Gesamt</td><td><%= html(formatBytes(diskTotal)) %></td></tr><tr><td>Frei</td><td><%= html(formatBytes(diskFree)) %></td></tr><tr><td>Nutzbar für Tomcat-Prozess</td><td><%= html(formatBytes(diskUsable)) %> <%= diskLow ? tag("weniger als 1 GB", "warn") : tag("ok", "ok") %></td></tr></tbody></table></details>
<details class="box" open><summary>Datenbank / job_log</summary><table class="table is-fullwidth diagnostics-table"><tbody><%= dbInfoHtml %><tr><td>Direkte DB-Prüfung</td><td><%= dbDirectAvailable ? tag("verfügbar", "ok") : tag("nicht verfügbar", "warn") %><% if (!dbDirectAvailable) { %><div class="diag-hint"><%= html(dbDiagnosticMessage) %></div><% } %></td></tr><tr><td>Tabelle db_version</td><td><%= dbVersionTableTag %><div class="diag-hint">Wird als Fallback für installierte Modulversionen verwendet, wenn /etl/list keine Version liefert.</div></td></tr><tr><td>Tabelle public.job_log</td><td><%= jobLogTableTag %></td></tr><tr><td>Spalte job_instance_id</td><td><%= jobLogHasJobInstanceId ? tag("vorhanden", "ok") : (jobLogExists ? tag("fehlt", "warn") : tag("nicht geprüft", "warn")) %></td></tr><tr><td>Spalte log</td><td><%= jobLogHasLog ? tag("vorhanden", "ok") : (jobLogExists ? tag("fehlt", "warn") : tag("nicht geprüft", "warn")) %></td></tr><tr><td>Spalte status</td><td><%= jobLogHasStatus ? tag("vorhanden", "ok") : (jobLogExists ? tag("fehlt / optional", "info") : tag("nicht geprüft", "warn")) %></td></tr></tbody></table><% if (jobLogColumnsHtml != null && jobLogColumnsHtml.length() > 0) { %><details style="margin-top:.75rem;"><summary>Spalten von public.job_log anzeigen</summary><table class="table is-fullwidth is-striped diagnostics-table"><thead><tr><th>Spalte</th><th>Datentyp</th></tr></thead><tbody><%= jobLogColumnsHtml %></tbody></table></details><% } %><% if (dbVersionRowsHtml != null && dbVersionRowsHtml.length() > 0) { %><details style="margin-top:.75rem;"><summary>Einträge aus public.db_version anzeigen</summary><table class="table is-fullwidth is-striped diagnostics-table"><thead><tr><th>his_system</th><th>version</th><th>systeminfo_id</th></tr></thead><tbody><%= dbVersionRowsHtml %></tbody></table></details><% } %><div class="diag-hint">Hinweis: Wenn /ds/api/etl/jobLog/{id} HTTP 500 liefert, kann eine fehlende oder unvollständige Tabelle job_log eine mögliche Ursache sein.</div></details>
<details class="box"><summary>SQL-Vorschlag: job_log anlegen</summary><div class="diag-hint">Nur zum Kopieren. Die Diagnose führt diesen SQL nicht aus.</div><div class="diag-actions"><button class="button is-light is-small js-copy-text" type="button" data-copy-target="jobLogCreateSql">SQL kopieren</button></div><textarea id="jobLogCreateSql" class="diag-textarea" readonly>CREATE TABLE IF NOT EXISTS job_log
(
job_instance_id bigint NOT NULL,
log text,
status text
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_job_log1
ON public.job_log USING btree (job_instance_id);</textarea></details>
<details class="box"><summary>Bekannte Fehlerbilder und Hinweise</summary><table class="table is-fullwidth diagnostics-table"><tbody><tr><td>/jobLog/{id} HTTP 500</td><td>Mögliche Ursachen: Tabelle job_log fehlt, Spalten fehlen, Logeintrag ist noch nicht geschrieben oder der Backend-Endpunkt fragt job_instance_id ab, während die Oberfläche eine JobExecutionId übergibt.</td></tr><tr><td>/job/list/{modul} HTTP 500</td><td>Mögliche Ursachen: Moduldateien fehlen, Tomcat lädt eine andere Webapp als erwartet, alte Modulverzeichnisse liegen noch unter db/module oder JVM-Optionen für Kettle fehlen.</td></tr><tr><td>/job/list/{modul} HTTP 404</td><td>Mögliche Ursachen: falscher Context-Pfad, falscher Port/Tunnel oder anderer Tomcat/Webapp-Kontext aktiv.</td></tr><tr><td>Log lädt erst beim zweiten Klick</td><td>Mögliche Ursachen: großer Logtext oder der Eintrag in job_log ist direkt nach COMPLETED noch nicht vollständig verfügbar.</td></tr></tbody></table><div class="small muted">Diese Hinweise sind bewusst vorsichtig formuliert. Sie nennen mögliche Ursachen, nicht zwingend die tatsächliche Ursache.</div></details>
<details class="box"><summary>Shell-Hinweise für Administratoren</summary><div class="diag-actions"><button class="button is-light is-small js-copy-text" type="button" data-copy-target="shellHints">Shell-Kommandos kopieren</button></div><textarea id="shellHints" class="diag-textarea" readonly>journalctl -u tomcat -n 300 --no-pager | egrep -i "ERROR|Exception|Caused by|jobLog|job/list"
grep -R "docBase\|appBase\|JAVA_OPTS\|CATALINA_OPTS" /etc/tomcat* /etc/sysconfig /etc/default 2>/dev/null
find <%= html(servletRealPath == null ? "/home/superx/webapps/superx" : servletRealPath) %> -type f -mmin -180 -ls</textarea></details>
</div>
</div>
<div class="box p-0"><table class="table is-fullwidth is-hoverable"><thead><tr><th>Modul</th><th>Status</th><th>Versionen</th><th>Letztes Update</th><th>Datenquelle</th><th>DB-Verbindung</th><th>Aktionen</th></tr></thead><tbody id="moduleTbody"><tr><td colspan="7" class="muted">Module werden geladen...</td></tr></tbody></table></div>
<div id="detailsPanel" class="box hidden"><div class="panel-head"><h2 id="detailsTitle">Details</h2><button id="closeDetailsBtn" class="button is-light" type="button">Schließen</button></div><div id="detailsBody" class="panel-body"></div></div>
<div class="box"><div class="panel-head"><h2>Aktueller Lauf</h2><div><button id="refreshRunBtn" class="button is-light" type="button">Status aktualisieren</button> <button id="stopRunBtn" class="button is-danger is-light" type="button" disabled>Stoppen</button></div></div><div class="panel-body"><div class="run-grid"><div class="muted">JobExecutionId</div><div id="runId">-</div><div class="muted">Job</div><div id="runJob">-</div><div class="muted">Status</div><div id="runStatus">Kein aktiver Lauf bekannt.</div><div class="muted">Fortschritt</div><div><div class="progress-wrap"><div id="progressBar" class="progress-bar"></div></div><div id="progressText" class="small muted" style="margin-top:6px;">-</div></div></div></div></div>
<div class="box"><div class="panel-head"><h2>Log-Ausgabe</h2><span id="logHint" class="small muted">Noch kein Log geladen.</span></div><div class="panel-body"><div class="log-actions"><button id="loadLogBtn" class="button is-light" type="button" disabled>Log laden</button><button id="clearLogBtn" class="button is-light" type="button">Log leeren</button></div><pre id="logOutput" class="log">Noch kein Lauf gestartet.</pre></div></div>
</div>
<script>
(function(){
'use strict';
var API_BASE='/superx/ds/api/etl';
var TOOLTIP_UNLOAD='Entladen aus dem Vorsystem, sofern angebunden';
var TOOLTIP_UPDATE='Laden der Rohdaten (Hauptladeroutine)';
var TOOLTIP_UNLOAD_UPDATE='Unload und Update hintereinander';
var TOOLTIP_UPGRADE='Modulaktualisierung (Upgrade der Datenstrukturen auf den ausgelieferten Stand)';
var DB_VERSION_FALLBACK=<%= dbVersionFallbackJson %>;
var AVAILABLE_VERSION_FALLBACK=<%= availableVersionFallbackJson %>;
var modules=[];
var jobsByComponent={};
var jobErrorsByComponent={};
var currentExecutionId=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')};
function esc(v){if(v===null||v===undefined){return '';}return String(v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
function attr(v){return esc(v).replace(/`/g,'&#96;');}
function code(v){return '<span class="code">'+esc(v)+'</span>';}
function badge(t,type){var cls='is-light';if(type==='ok'){cls='is-success is-light';}else if(type==='warn'){cls='is-warning is-light';}else if(type==='error'){cls='is-danger is-light';}return '<span class="tag '+cls+'">'+esc(t)+'</span>';}
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 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 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;}
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 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 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 versionInfo(m){var installedKeys=['installedVersion','currentVersion','dbVersion','databaseVersion','componentVersion','moduleVersion','version','installed_version','current_version','db_version'];var targetKeys=['upgradeVersion','targetVersion','availableVersion','latestVersion','packageVersion','fileVersion','newVersion','upgrade_version','target_version','available_version','latest_version','package_version','file_version','new_version'];function first(keys){for(var i=0;i<keys.length;i++){if(isDeliveredVersion(m[keys[i]])){return String(m[keys[i]]).trim();}}return null;}function dbFallback(){var abbr=key(m).toLowerCase();if(abbr&&DB_VERSION_FALLBACK&&isDeliveredVersion(DB_VERSION_FALLBACK[abbr])){return String(DB_VERSION_FALLBACK[abbr]).trim();}if(m.systeminfoId!==undefined&&m.systeminfoId!==null&&DB_VERSION_FALLBACK&&isDeliveredVersion(DB_VERSION_FALLBACK['sid:'+m.systeminfoId])){return String(DB_VERSION_FALLBACK['sid:'+m.systeminfoId]).trim();}return null;}function availableInfo(){var abbr=key(m).toLowerCase();if(abbr&&AVAILABLE_VERSION_FALLBACK&&AVAILABLE_VERSION_FALLBACK[abbr]){return AVAILABLE_VERSION_FALLBACK[abbr];}return {};}
var raw=[];for(var k in m){if(Object.prototype.hasOwnProperty.call(m,k)&&k.toLowerCase().indexOf('version')>=0){raw.push(k+'='+m[k]);}}
var jsonInstalled=first(installedKeys);var dbVersion=dbFallback();var installed=jsonInstalled;var installedSource=jsonInstalled?'json':'';if(!installed&&dbVersion){installed=dbVersion;installedSource='db_version';}
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';}
return {installed:installed,target:target,raw:raw,installedSource:installedSource,targetSource:targetSource,jsonInstalled:jsonInstalled,dbVersion:dbVersion,jsonTarget:jsonTarget,xmlVersion:xmlVersion,xmlPath:ai.xmlPath||'',versionFileVersion:versionFileVersion,versionFilePath:ai.versionFilePath||''};}
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 fmtDate(v){if(v===null||v===undefined||v===''||v===0){return '<span class="muted">-</span>';}var n=Number(v);if(isNaN(n)){return esc(v);}var d=new Date(n);if(!isNaN(d.getTime())){if(d.getFullYear()<1971){return '<span class="muted">nicht gesetzt</span>';}return esc(d.toLocaleString('de-DE'));}return esc(v);}
function renderModules(){var q=(el.searchInput.value||'').toLowerCase(),filter=el.filterSelect.value,rows=[],shown=0;for(var i=0;i<modules.length;i++){var m=modules[i],abbr=key(m),vi=versionInfo(m),displayDb=displayDataSource(m),hay=[m.name||'',abbr,m.database||'',displayDb,m.systeminfoId||'',vi.installed||'',vi.target||''].join(' ').toLowerCase();if(q&&hay.indexOf(q)<0){continue;}var cp=completePlan(m),ul=unloadPlan(m),uo=updateOnlyPlan(m),ug=upgradePlan(m);if(filter==='installed'&&m.installed!==true){continue;}if(filter==='complete'&&!cp.available){continue;}if(filter==='unload'&&!ul.available){continue;}if(filter==='update'&&!uo.available){continue;}if(filter==='dbok'&&m.databaseConnected!==true){continue;}if(filter==='dberror'&&m.databaseConnected===true){continue;}shown++;var moduleHtml='<div class="module-name">'+esc(m.name||abbr||'-')+'</div><div class="module-line">Kürzel: '+code(abbr||'-')+'</div><div class="module-line">Systeminfo-ID: '+esc(m.systeminfoId||'-')+'</div>';var status=m.installed===true?badge('installiert','ok'):badge('nicht installiert','warn');var versionHtml='<div class="version-line">installiert: '+versionValue(vi.installed)+'</div><div class="version-line">verfügbar: '+versionValue(vi.target)+'</div>';var db=m.databaseConnected===true?badge('verbunden','ok'):(m.databaseConnected===false?badge('nicht verbunden','warn'):badge('unbekannt','neutral'));var ds=esc(displayDb);var actions='<div class="action-row">';actions+=ul.available?'<button class="button is-light is-small js-unload" type="button" title="'+attr(TOOLTIP_UNLOAD)+'" aria-label="Unload: '+attr(TOOLTIP_UNLOAD)+'" data-component="'+attr(abbr)+'">Unload</button>':'<button class="button is-light is-small" type="button" title="'+attr(TOOLTIP_UNLOAD)+'" aria-label="Unload: '+attr(TOOLTIP_UNLOAD)+'" disabled>Unload</button>';actions+=uo.available?'<button class="button is-light is-small js-updateonly" type="button" title="'+attr(TOOLTIP_UPDATE)+'" aria-label="Update: '+attr(TOOLTIP_UPDATE)+'" data-component="'+attr(abbr)+'">Update</button>':'<button class="button is-light is-small" type="button" title="'+attr(TOOLTIP_UPDATE)+'" aria-label="Update: '+attr(TOOLTIP_UPDATE)+'" disabled>Update</button>';actions+=cp.available?'<button class="button is-primary is-small js-complete" type="button" title="'+attr(TOOLTIP_UNLOAD_UPDATE)+'" aria-label="Unload & Update: '+attr(TOOLTIP_UNLOAD_UPDATE)+'" data-component="'+attr(abbr)+'">Unload &amp; Update</button>':'<button class="button is-light is-small" type="button" title="'+attr(TOOLTIP_UNLOAD_UPDATE)+'" aria-label="Unload & Update: '+attr(TOOLTIP_UNLOAD_UPDATE)+'" disabled>Unload &amp; Update</button>';actions+=ug.available?'<button class="button is-warning is-light is-small js-upgrade" type="button" title="'+attr(TOOLTIP_UPGRADE)+'" aria-label="Upgrade: '+attr(TOOLTIP_UPGRADE)+'" data-component="'+attr(abbr)+'">Upgrade</button>':'<button class="button is-light is-small" type="button" title="'+attr(TOOLTIP_UPGRADE)+'" aria-label="Upgrade: '+attr(TOOLTIP_UPGRADE)+'" disabled>Upgrade</button>';actions+='<button class="button is-info is-light is-small js-details" type="button" data-component="'+attr(abbr)+'">Details</button></div>';rows.push('<tr><td>'+moduleHtml+'</td><td>'+status+'</td><td>'+versionHtml+'</td><td>'+fmtDate(m.lastUpdate)+'</td><td>'+ds+'</td><td>'+db+'</td><td class="action-cell">'+actions+'</td></tr>');}el.moduleCount.textContent=shown+' von '+modules.length+' Modulen angezeigt';el.moduleTbody.innerHTML=rows.length?rows.join(''):'<tr><td colspan="7" class="muted">Keine Module gefunden.</td></tr>';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');};}bs=document.querySelectorAll('.js-unload');for(var j=0;j<bs.length;j++){bs[j].onclick=function(){startPlan(findModule(this.getAttribute('data-component')),unloadPlan,'Unload');};}bs=document.querySelectorAll('.js-updateonly');for(var k=0;k<bs.length;k++){bs[k].onclick=function(){startPlan(findModule(this.getAttribute('data-component')),updateOnlyPlan,'Update');};}bs=document.querySelectorAll('.js-upgrade');for(var l=0;l<bs.length;l++){bs[l].onclick=function(){startUpgrade(findModule(this.getAttribute('data-component')));};}bs=document.querySelectorAll('.js-details');for(var m=0;m<bs.length;m++){bs[m].onclick=function(){showDetails(findModule(this.getAttribute('data-component')));};}}
function findModule(abbr){for(var i=0;i<modules.length;i++){if(key(modules[i])===abbr){return modules[i];}}return null;}
function renderPlan(title,p){var h='<div class="box"><h3>'+esc(title)+'</h3><div class="plan-grid">';h+='<div class="muted">verfügbar</div><div>'+(p.available?badge('ja','ok'):badge('nein','warn'))+'</div>';h+='<div class="muted">gesucht</div><div>'+code(p.searched||'-')+'</div>';h+='<div class="muted">Job</div><div>'+(p.job&&p.job.id?code(p.job.id):'<span class="muted">-</span>')+'</div>';h+='<div class="muted">Start</div><div>'+(p.endpoint?code(p.endpoint):'<span class="muted">-</span>')+'</div>';h+='<div class="muted">Erklärung</div><div>'+esc(p.reason||'-')+'</div></div></div>';return h;}
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 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);});}
function versionDiagValue(v,emptyText){return isDeliveredVersion(v)?esc(v):'<span class="muted">'+esc(emptyText||'nicht geliefert')+'</span>';}
function renderVersionDiagnostics(){if(!el.diagnosticsVersions){return;}if(!modules||modules.length===0){el.diagnosticsVersions.innerHTML='<div class="muted">Module sind noch nicht geladen.</div>';return;}var h='<table class="table is-fullwidth is-striped diagnostics-table"><thead><tr><th>Modul</th><th>Installierte Version</th><th>Verfügbare Version</th></tr></thead><tbody>';for(var i=0;i<modules.length;i++){var m=modules[i],abbr=key(m),vi=versionInfo(m);h+='<tr><td><strong>'+esc(m.name||abbr||'-')+'</strong><div class="small muted">Kürzel: '+esc(abbr||'-')+' / Systeminfo-ID: '+esc(m.systeminfoId||'-')+'</div></td>';h+='<td><div>REST / JSON: '+versionDiagValue(vi.jsonInstalled,'nicht geliefert')+'</div><div>db_version: '+versionDiagValue(vi.dbVersion,'kein Eintrag gefunden')+'</div><div><strong>verwendet:</strong> '+versionDiagValue(vi.installed,'nicht geliefert')+'</div></td>';h+='<td><div>REST / JSON: '+versionDiagValue(vi.jsonTarget,'nicht geliefert')+'</div><div>Modul-XML: '+versionDiagValue(vi.xmlVersion,'nicht gefunden')+'</div><div class="small muted">XML-Pfad: '+(vi.xmlPath?code(vi.xmlPath):'<span class="muted">nicht ermittelbar</span>')+'</div><div>VERSION-Datei: '+versionDiagValue(vi.versionFileVersion,'nicht gefunden')+'</div><div class="small muted">VERSION-Pfad: '+(vi.versionFilePath?code(vi.versionFilePath):'<span class="muted">nicht ermittelbar</span>')+'</div><div><strong>verwendet:</strong> '+versionDiagValue(vi.target,'nicht geliefert')+'</div></td></tr>';}
h+='</tbody></table>';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 '<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 runDiagnostics(){renderVersionDiagnostics();if(!el.diagnosticsRest){return;}el.diagnosticsRest.innerHTML='<div class="muted">REST-Schnittstellen werden geprüft...</div>';var checks=[];checks.push({label:'/etl/list',url:API_BASE+'/list'});checks.push({label:'/running',url:API_BASE+'/running'});for(var i=0;i<modules.length;i++){var abbr=key(modules[i]);if(abbr){checks.push({label:'/job/list/'+abbr,url:API_BASE+'/job/list/'+encodeURIComponent(abbr)});}}if(currentExecutionId){checks.push({label:'/jobExecutionStatus/'+currentExecutionId,url:API_BASE+'/jobExecutionStatus/'+encodeURIComponent(currentExecutionId)});checks.push({label:'/jobLog/'+currentExecutionId,url:API_BASE+'/jobLog/'+encodeURIComponent(currentExecutionId)});}else{checks.push({label:'/jobExecutionStatus/{id}',skip:true,detail:'Keine aktuelle JobExecutionId bekannt.'});checks.push({label:'/jobLog/{id}',skip:true,detail:'Keine aktuelle JobExecutionId bekannt.'});}var results=[];function next(idx){if(idx>=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;i<keys.length;i++){if(r[keys[i]]!==undefined&&r[keys[i]]!==null){return parseId(r[keys[i]]);}}}return null;}
function stringify(v){if(typeof v==='string'){return v;}try{return JSON.stringify(v,null,2);}catch(e){return String(v);}}
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 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');});}
el.reloadBtn.onclick=loadModules;el.searchInput.oninput=renderModules;el.filterSelect.onchange=renderModules;el.diagnosticsBtn.onclick=function(){el.diagnosticsPanel.classList.remove('hidden');runDiagnostics();el.diagnosticsPanel.scrollIntoView({behavior:'smooth',block:'start'});};el.refreshDiagnosticsBtn.onclick=runDiagnostics;el.closeDiagnosticsBtn.onclick=function(){el.diagnosticsPanel.classList.add('hidden');};document.addEventListener('click',function(ev){var t=ev.target;if(t&&t.classList&&t.classList.contains('js-copy-text')){copyFromElement(t.getAttribute('data-copy-target'));}});el.closeDetailsBtn.onclick=function(){el.detailsPanel.classList.add('hidden');};el.refreshRunBtn.onclick=function(){if(currentExecutionId){pollStatus();}else{checkRunning(false);}};el.stopRunBtn.onclick=stopRun;el.loadLogBtn.onclick=function(){loadLog(true);};el.clearLogBtn.onclick=function(){setLog('','Log geleert.');};
loadModules().then(function(){checkRunning(false);});
})();
</script>
</body>
</html>