some fixes
This commit is contained in:
parent
30eed4c3e7
commit
1fe1137f80
@ -18,6 +18,7 @@ import java.sql.PreparedStatement;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -36,9 +37,9 @@ public class Main {
|
|||||||
server.setExecutor(null);
|
server.setExecutor(null);
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
log("Listening on http://0.0.0.0:8080");
|
log("Listening on http://127.0.0.1:8080");
|
||||||
log("Dashboard: http://0.0.0.0:8080/hb/dashboard");
|
log("Dashboard: http://127.0.0.1:8080/hb/dashboard");
|
||||||
log("API endpoint: http://0.0.0.0:8080/hb/api");
|
log("API endpoint: http://127.0.0.1:8080/hb/api");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class HeartbeatHandler implements HttpHandler {
|
private static final class HeartbeatHandler implements HttpHandler {
|
||||||
@ -148,7 +149,7 @@ public class Main {
|
|||||||
for (String processName : processes) {
|
for (String processName : processes) {
|
||||||
statement.setString(1, request.machineName);
|
statement.setString(1, request.machineName);
|
||||||
statement.setString(2, request.status);
|
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);
|
statement.setString(4, processName);
|
||||||
inserted += statement.executeUpdate();
|
inserted += statement.executeUpdate();
|
||||||
}
|
}
|
||||||
@ -201,48 +202,68 @@ public class Main {
|
|||||||
name = name.substring(0, dotIndex);
|
name = name.substring(0, dotIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seřadit všechny názvy podle délky (od nejkratšího)
|
// Najít všechny unikátní názvy bez .exe
|
||||||
java.util.List<String> sortedNames = new java.util.ArrayList<>(allProcessNames);
|
java.util.Set<String> uniqueNames = new java.util.TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
sortedNames.sort((a, b) -> {
|
for (String procName : allProcessNames) {
|
||||||
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<String> baseNames = new java.util.TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
|
||||||
|
|
||||||
for (String procName : sortedNames) {
|
|
||||||
if (procName == null || procName.isBlank()) continue;
|
if (procName == null || procName.isBlank()) continue;
|
||||||
String cleanName = procName;
|
int dotIdx = procName.lastIndexOf('.');
|
||||||
int dotIdx = cleanName.lastIndexOf('.');
|
|
||||||
if (dotIdx > 0) {
|
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
|
// Najít skupinu procesů, které patří k tomuto (mají společnou předponu)
|
||||||
boolean foundBase = false;
|
java.util.List<String> group = new java.util.ArrayList<>();
|
||||||
for (String base : baseNames) {
|
for (String n : uniqueNames) {
|
||||||
if (cleanName.toLowerCase().startsWith(base.toLowerCase())) {
|
// Zkontrolovat, zda název začíná na stejný základ jako target nebo naopak
|
||||||
foundBase = true;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isPrefixOfAll) {
|
||||||
if (!foundBase) {
|
return shortest;
|
||||||
baseNames.add(cleanName);
|
|
||||||
}
|
}
|
||||||
|
shortest = shortest.substring(0, shortest.length() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Najít základ pro tento konkrétní název
|
return name;
|
||||||
for (String base : baseNames) {
|
|
||||||
if (name.toLowerCase().startsWith(base.toLowerCase())) {
|
|
||||||
return base.trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private StatsResponse getStats(String machine, String process, String status, String from, String to) throws SQLException {
|
||||||
@ -310,13 +331,19 @@ public class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private LastRecordTimeResponse getLastRecordTime() throws SQLException {
|
private LastRecordTimeResponse getLastRecordTime(String date) throws SQLException {
|
||||||
String sql = "SELECT detected_at FROM process_heartbeat ORDER BY detected_at DESC LIMIT 1";
|
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);
|
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword);
|
||||||
PreparedStatement stmt = connection.prepareStatement(sql);
|
PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
java.sql.ResultSet rs = stmt.executeQuery()) {
|
|
||||||
|
|
||||||
|
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()) {
|
if (rs.next()) {
|
||||||
Timestamp ts = rs.getTimestamp(1);
|
Timestamp ts = rs.getTimestamp(1);
|
||||||
return new LastRecordTimeResponse(ts.toInstant().toString());
|
return new LastRecordTimeResponse(ts.toInstant().toString());
|
||||||
@ -325,6 +352,7 @@ public class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class FilterOptions {
|
private static final class FilterOptions {
|
||||||
List<String> machines;
|
List<String> machines;
|
||||||
@ -514,7 +542,8 @@ public class Main {
|
|||||||
String response = GSON.toJson(database.getFilterOptions());
|
String response = GSON.toJson(database.getFilterOptions());
|
||||||
sendJson(exchange, 200, response);
|
sendJson(exchange, 200, response);
|
||||||
} else if ("lastRecordTime".equals(type)) {
|
} 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);
|
sendJson(exchange, 200, response);
|
||||||
} else if ("stats".equals(type)) {
|
} else if ("stats".equals(type)) {
|
||||||
String machine = getParam(query, "machine");
|
String machine = getParam(query, "machine");
|
||||||
|
|||||||
@ -56,16 +56,17 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 10px;
|
gap: 15px;
|
||||||
|
align-items: end;
|
||||||
}
|
}
|
||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
.filter-group label {
|
.filter-group label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.filter-group select,
|
.filter-group select,
|
||||||
@ -74,9 +75,7 @@
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
width: 100%;
|
||||||
.filter-group input {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
.filter-buttons {
|
.filter-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -130,6 +129,46 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
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 {
|
.error {
|
||||||
background: #fee;
|
background: #fee;
|
||||||
color: #c33;
|
color: #c33;
|
||||||
@ -166,13 +205,18 @@
|
|||||||
<option value="">-- Všechny stavy --</option>
|
<option value="">-- Všechny stavy --</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-buttons" style="grid-column: 1 / -1;">
|
||||||
|
<button class="reset" onclick="resetFilters()">Reset</button>
|
||||||
|
<button onclick="applyFilters()">Použít filtry</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="date-filters">
|
||||||
<label for="selectedDate">Den:</label>
|
<label for="selectedDate">Den:</label>
|
||||||
<input type="date" id="selectedDate">
|
<input type="date" id="selectedDate">
|
||||||
</div>
|
<div class="date-buttons">
|
||||||
<div class="filter-buttons" style="grid-column: 1 / -1;">
|
<button onclick="changeDate(-1)">Den-</button>
|
||||||
<button onclick="applyFilters()">Použít filtry</button>
|
<button onclick="changeDate(0)">Dnes</button>
|
||||||
<button class="reset" onclick="resetFilters()">Reset</button>
|
<button onclick="changeDate(1)">Den+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -202,6 +246,13 @@
|
|||||||
return normalized;
|
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() {
|
async function loadFilters() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/hb/api/data?type=filters&apiKey=' + encodeURIComponent(apiKey));
|
const response = await axios.get('/hb/api/data?type=filters&apiKey=' + encodeURIComponent(apiKey));
|
||||||
@ -276,6 +327,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateCharts(data) {
|
function updateCharts(data) {
|
||||||
|
loadLastRecordTime();
|
||||||
const records = data.records;
|
const records = data.records;
|
||||||
|
|
||||||
// Agregace času podle procesů
|
// Agregace času podle procesů
|
||||||
@ -296,20 +348,11 @@
|
|||||||
processByName[mainName].push(r);
|
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 => {
|
Object.keys(processByName).forEach(mainName => {
|
||||||
const records = processByName[mainName];
|
const records = processByName[mainName];
|
||||||
let totalTimeMs = 0;
|
const totalSeconds = records.length * 45;
|
||||||
|
processTimeMap[mainName] = totalSeconds / 60;
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (processTimeChart) processTimeChart.destroy();
|
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() {
|
function resetFilters() {
|
||||||
document.getElementById('machine').value = '';
|
document.getElementById('machine').value = '';
|
||||||
document.getElementById('process').value = '';
|
document.getElementById('process').value = '';
|
||||||
@ -380,10 +443,16 @@
|
|||||||
|
|
||||||
window.addEventListener('load', loadFilters);
|
window.addEventListener('load', loadFilters);
|
||||||
document.getElementById('selectedDate')?.addEventListener('change', applyFilters);
|
document.getElementById('selectedDate')?.addEventListener('change', applyFilters);
|
||||||
|
document.getElementById('selectedDate')?.addEventListener('change', loadLastRecordTime);
|
||||||
|
|
||||||
async function loadLastRecordTime() {
|
async function loadLastRecordTime() {
|
||||||
try {
|
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;
|
const data = response.data;
|
||||||
if (data.lastRecordTime) {
|
if (data.lastRecordTime) {
|
||||||
const date = new Date(data.lastRecordTime);
|
const date = new Date(data.lastRecordTime);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user