diff --git a/java-api/src/main/java/cz/kamma/processmonitor/Main.java b/java-api/src/main/java/cz/kamma/processmonitor/Main.java index 2eff174..4b322e8 100644 --- a/java-api/src/main/java/cz/kamma/processmonitor/Main.java +++ b/java-api/src/main/java/cz/kamma/processmonitor/Main.java @@ -18,6 +18,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.time.Instant; +import java.time.ZoneId; import java.time.format.DateTimeParseException; import java.util.Collections; import java.util.List; @@ -36,9 +37,9 @@ public class Main { server.setExecutor(null); server.start(); - log("Listening on http://0.0.0.0:8080"); - log("Dashboard: http://0.0.0.0:8080/hb/dashboard"); - log("API endpoint: http://0.0.0.0:8080/hb/api"); + log("Listening on http://127.0.0.1:8080"); + log("Dashboard: http://127.0.0.1:8080/hb/dashboard"); + log("API endpoint: http://127.0.0.1:8080/hb/api"); } private static final class HeartbeatHandler implements HttpHandler { @@ -148,7 +149,7 @@ public class Main { for (String processName : processes) { statement.setString(1, request.machineName); statement.setString(2, request.status); - statement.setTimestamp(3, Timestamp.from(request.detectedAt)); + statement.setTimestamp(3, Timestamp.from(request.detectedAt.atZone(ZoneId.systemDefault()).toInstant())); statement.setString(4, processName); inserted += statement.executeUpdate(); } @@ -201,48 +202,68 @@ public class Main { name = name.substring(0, dotIndex); } - // Seřadit všechny názvy podle délky (od nejkratšího) - java.util.List sortedNames = new java.util.ArrayList<>(allProcessNames); - sortedNames.sort((a, b) -> { - int lenA = a != null ? a.length() : 0; - int lenB = b != null ? b.length() : 0; - return Integer.compare(lenA, lenB); - }); - - // Pro každý název najít jeho unikátní základ - // Pokud nějaký název začíná kratším názvem, odstraníme ten delší - java.util.Set baseNames = new java.util.TreeSet<>(String.CASE_INSENSITIVE_ORDER); - - for (String procName : sortedNames) { + // Najít všechny unikátní názvy bez .exe + java.util.Set uniqueNames = new java.util.TreeSet<>(String.CASE_INSENSITIVE_ORDER); + for (String procName : allProcessNames) { if (procName == null || procName.isBlank()) continue; - String cleanName = procName; - int dotIdx = cleanName.lastIndexOf('.'); + int dotIdx = procName.lastIndexOf('.'); if (dotIdx > 0) { - cleanName = cleanName.substring(0, dotIdx); + uniqueNames.add(procName.substring(0, dotIdx)); + } else { + uniqueNames.add(procName); } + } - // Zkontrolovat, zda tento název začíná nějakým již přidaným základem - boolean foundBase = false; - for (String base : baseNames) { - if (cleanName.toLowerCase().startsWith(base.toLowerCase())) { - foundBase = true; + // Najít skupinu procesů, které patří k tomuto (mají společnou předponu) + java.util.List group = new java.util.ArrayList<>(); + for (String n : uniqueNames) { + // Zkontrolovat, zda název začíná na stejný základ jako target nebo naopak + if (hasCommonPrefix(name, n)) { + group.add(n); + } + } + + // Pokud je jen jeden v group, vrať ho + if (group.size() == 1) { + return group.get(0); + } + + // Najít nejkratší společnou předponu pro tuto skupinu + group.sort((a, b) -> Integer.compare(a.length(), b.length())); + String shortest = group.get(0); + + while (shortest.length() > 0) { + boolean isPrefixOfAll = true; + for (String n : group) { + if (!n.toLowerCase().startsWith(shortest.toLowerCase())) { + isPrefixOfAll = false; break; } } - - if (!foundBase) { - baseNames.add(cleanName); + if (isPrefixOfAll) { + return shortest; } + shortest = shortest.substring(0, shortest.length() - 1); } - // Najít základ pro tento konkrétní název - for (String base : baseNames) { - if (name.toLowerCase().startsWith(base.toLowerCase())) { - return base.trim(); - } - } + return name; + } - return name.trim(); + private boolean hasCommonPrefix(String a, String b) { + // Dva názvy patří do stejné skupiny, pokud jeden začíná na druhý + // nebo mají společnou předponu alespoň 3 znaky + return a.toLowerCase().startsWith(b.toLowerCase()) + || b.toLowerCase().startsWith(a.toLowerCase()) + || getCommonPrefix(a, b).length() >= 3; + } + + private String getCommonPrefix(String a, String b) { + int minLen = Math.min(a.length(), b.length()); + int i = 0; + while (i < minLen && a.toLowerCase().charAt(i) == b.toLowerCase().charAt(i)) { + i++; + } + return a.substring(0, i); } private StatsResponse getStats(String machine, String process, String status, String from, String to) throws SQLException { @@ -310,18 +331,25 @@ public class Main { } } - private LastRecordTimeResponse getLastRecordTime() throws SQLException { - String sql = "SELECT detected_at FROM process_heartbeat ORDER BY detected_at DESC LIMIT 1"; + private LastRecordTimeResponse getLastRecordTime(String date) throws SQLException { + String sql = "SELECT detected_at FROM process_heartbeat WHERE DATE(detected_at) = ? ORDER BY detected_at DESC LIMIT 1"; try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword); - PreparedStatement stmt = connection.prepareStatement(sql); - java.sql.ResultSet rs = stmt.executeQuery()) { + PreparedStatement stmt = connection.prepareStatement(sql)) { - if (rs.next()) { - Timestamp ts = rs.getTimestamp(1); - return new LastRecordTimeResponse(ts.toInstant().toString()); + if (date != null && !date.isBlank()) { + stmt.setString(1, date); + } else { + stmt.setString(1, java.time.LocalDate.now(ZoneId.systemDefault()).toString()); + } + + try (java.sql.ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + Timestamp ts = rs.getTimestamp(1); + return new LastRecordTimeResponse(ts.toInstant().toString()); + } + return new LastRecordTimeResponse(null); } - return new LastRecordTimeResponse(null); } } } @@ -514,7 +542,8 @@ public class Main { String response = GSON.toJson(database.getFilterOptions()); sendJson(exchange, 200, response); } else if ("lastRecordTime".equals(type)) { - String response = GSON.toJson(database.getLastRecordTime()); + String date = getParam(query, "date"); + String response = GSON.toJson(database.getLastRecordTime(date)); sendJson(exchange, 200, response); } else if ("stats".equals(type)) { String machine = getParam(query, "machine"); diff --git a/java-api/src/main/resources/dashboard.html b/java-api/src/main/resources/dashboard.html index c4d2430..1738119 100644 --- a/java-api/src/main/resources/dashboard.html +++ b/java-api/src/main/resources/dashboard.html @@ -56,16 +56,17 @@ border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; + grid-template-columns: repeat(4, 1fr); + gap: 15px; + align-items: end; } .filter-group { display: flex; flex-direction: column; + gap: 5px; } .filter-group label { font-weight: 600; - margin-bottom: 5px; font-size: 14px; } .filter-group select, @@ -74,9 +75,7 @@ border: 1px solid #ddd; border-radius: 4px; font-size: 14px; - } - .filter-group input { - margin-bottom: 5px; + width: 100%; } .filter-buttons { display: flex; @@ -130,6 +129,46 @@ font-weight: 600; color: #333; } + .date-buttons { + display: flex; + gap: 5px; + margin-top: 2px; + } + .date-buttons button { + flex: 1; + padding: 6px 8px; + background: #667eea; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background 0.3s; + } + .date-buttons button:hover { + background: #5568d3; + } + .date-filters { + background: white; + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + margin-top: 10px; + display: flex; + align-items: center; + gap: 15px; + } + .date-filters label { + font-weight: 600; + font-size: 14px; + white-space: nowrap; + } + .date-filters input { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + } .error { background: #fee; color: #c33; @@ -166,13 +205,18 @@ -
- - -
- + +
+ +
+ + +
+ + +
@@ -202,6 +246,13 @@ return normalized; } + function getLocalDateString(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + async function loadFilters() { try { const response = await axios.get('/hb/api/data?type=filters&apiKey=' + encodeURIComponent(apiKey)); @@ -276,6 +327,7 @@ } function updateCharts(data) { + loadLastRecordTime(); const records = data.records; // Agregace času podle procesů @@ -296,20 +348,11 @@ processByName[mainName].push(r); }); - // Spočítat čas běhu pro každý hlavní název + // Spočítat čas běhu pro každý hlavní název (45s za každý záznam) Object.keys(processByName).forEach(mainName => { const records = processByName[mainName]; - let totalTimeMs = 0; - - for (let i = 0; i < records.length - 1; i++) { - const current = new Date(records[i].detected_at); - const next = new Date(records[i + 1].detected_at); - totalTimeMs += (next - current); - } - - // Převést na minuty - const totalMinutes = Math.round(totalTimeMs / 60000); - processTimeMap[mainName] = totalMinutes; + const totalSeconds = records.length * 45; + processTimeMap[mainName] = totalSeconds / 60; }); if (processTimeChart) processTimeChart.destroy(); @@ -362,6 +405,26 @@ }); } + function changeDate(days) { + const dateInput = document.getElementById('selectedDate'); + const parts = dateInput.value.split('-'); + const year = parseInt(parts[0], 10); + const month = parseInt(parts[1], 10) - 1; // měsíce jsou 0-11 + const day = parseInt(parts[2], 10); + + const date = new Date(year, month, day); + + if (days === 0) { + // Pro "Dnes" použijeme přímo lokální datum + const today = new Date(); + dateInput.value = getLocalDateString(today); + } else { + date.setDate(date.getDate() + days); + dateInput.value = getLocalDateString(date); + } + applyFilters(); + } + function resetFilters() { document.getElementById('machine').value = ''; document.getElementById('process').value = ''; @@ -380,10 +443,16 @@ window.addEventListener('load', loadFilters); document.getElementById('selectedDate')?.addEventListener('change', applyFilters); + document.getElementById('selectedDate')?.addEventListener('change', loadLastRecordTime); async function loadLastRecordTime() { try { - const response = await axios.get('/hb/api/data?type=lastRecordTime&apiKey=' + encodeURIComponent(apiKey)); + const selectedDate = document.getElementById('selectedDate').value; + let url = '/hb/api/data?type=lastRecordTime&apiKey=' + encodeURIComponent(apiKey); + if (selectedDate) { + url += '&date=' + encodeURIComponent(selectedDate); + } + const response = await axios.get(url); const data = response.data; if (data.lastRecordTime) { const date = new Date(data.lastRecordTime);