better UI results

This commit is contained in:
Radek Davidek 2026-03-29 17:03:31 +02:00
parent e908f53e8b
commit 30eed4c3e7
4 changed files with 161 additions and 255 deletions

View File

@ -190,6 +190,61 @@ public class Main {
} }
} }
private String extractMainName(String processName, java.util.List<String> allProcessNames) {
if (processName == null || processName.isBlank()) {
return null;
}
// Odstranit .exe
String name = processName;
int dotIndex = name.lastIndexOf('.');
if (dotIndex > 0) {
name = name.substring(0, dotIndex);
}
// Seřadit všechny názvy podle délky (od nejkratšího)
java.util.List<String> 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<String> baseNames = new java.util.TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for (String procName : sortedNames) {
if (procName == null || procName.isBlank()) continue;
String cleanName = procName;
int dotIdx = cleanName.lastIndexOf('.');
if (dotIdx > 0) {
cleanName = cleanName.substring(0, dotIdx);
}
// 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;
break;
}
}
if (!foundBase) {
baseNames.add(cleanName);
}
}
// 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.trim();
}
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 {
StringBuilder sql = new StringBuilder("SELECT id, machine_name, status, detected_at, process_name FROM process_heartbeat WHERE 1=1"); StringBuilder sql = new StringBuilder("SELECT id, machine_name, status, detected_at, process_name FROM process_heartbeat WHERE 1=1");
java.util.List<String> params = new java.util.ArrayList<>(); java.util.List<String> params = new java.util.ArrayList<>();
@ -224,15 +279,29 @@ public class Main {
stmt.setString(i + 1, params.get(i)); stmt.setString(i + 1, params.get(i));
} }
// Nejprve načíst všechny unikátní názvy procesů pro určení hlavního názvu
java.util.List<String> allProcessNames = new java.util.ArrayList<>();
try (PreparedStatement allStmt = connection.prepareStatement(
"SELECT DISTINCT process_name FROM process_heartbeat WHERE process_name IS NOT NULL")) {
try (java.sql.ResultSet rs = allStmt.executeQuery()) {
while (rs.next()) {
allProcessNames.add(rs.getString(1));
}
}
}
java.util.List<Record> records = new java.util.ArrayList<>(); java.util.List<Record> records = new java.util.ArrayList<>();
try (java.sql.ResultSet rs = stmt.executeQuery()) { try (java.sql.ResultSet rs = stmt.executeQuery()) {
while (rs.next()) { while (rs.next()) {
String processName = rs.getString(5);
String mainName = extractMainName(processName, allProcessNames);
records.add(new Record( records.add(new Record(
rs.getLong(1), rs.getLong(1),
rs.getString(2), rs.getString(2),
rs.getString(3), rs.getString(3),
rs.getTimestamp(4), rs.getTimestamp(4),
rs.getString(5) processName,
mainName
)); ));
} }
} }
@ -240,6 +309,21 @@ public class Main {
return new StatsResponse(records); return new StatsResponse(records);
} }
} }
private LastRecordTimeResponse getLastRecordTime() throws SQLException {
String sql = "SELECT detected_at FROM process_heartbeat 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()) {
if (rs.next()) {
Timestamp ts = rs.getTimestamp(1);
return new LastRecordTimeResponse(ts.toInstant().toString());
}
return new LastRecordTimeResponse(null);
}
}
} }
private static final class FilterOptions { private static final class FilterOptions {
@ -260,13 +344,15 @@ public class Main {
String status; String status;
String detected_at; String detected_at;
String process_name; String process_name;
String main_name;
Record(long id, String machine_name, String status, Timestamp detected_at, String process_name) { Record(long id, String machine_name, String status, Timestamp detected_at, String process_name, String main_name) {
this.id = id; this.id = id;
this.machine_name = machine_name; this.machine_name = machine_name;
this.status = status; this.status = status;
this.detected_at = detected_at != null ? detected_at.toInstant().toString() : null; this.detected_at = detected_at != null ? detected_at.toInstant().toString() : null;
this.process_name = process_name; this.process_name = process_name;
this.main_name = main_name;
} }
} }
@ -278,6 +364,14 @@ public class Main {
} }
} }
private static final class LastRecordTimeResponse {
String lastRecordTime;
LastRecordTimeResponse(String lastRecordTime) {
this.lastRecordTime = lastRecordTime;
}
}
private static final class HeartbeatRequest { private static final class HeartbeatRequest {
private String machine_name; private String machine_name;
private String status; private String status;
@ -419,6 +513,9 @@ public class Main {
if ("filters".equals(type)) { if ("filters".equals(type)) {
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)) {
String response = GSON.toJson(database.getLastRecordTime());
sendJson(exchange, 200, response);
} else if ("stats".equals(type)) { } else if ("stats".equals(type)) {
String machine = getParam(query, "machine"); String machine = getParam(query, "machine");
String process = getParam(query, "process"); String process = getParam(query, "process");

View File

@ -27,12 +27,12 @@
align-items: center; align-items: center;
} }
.header h1 { .header h1 {
margin-bottom: 10px; margin-bottom: 5px;
} }
.header-content { .header-content {
flex: 1; flex: 1;
} }
.logout-btn { .logout-btn {
background: rgba(255,255,255,0.2); background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.3); border: 1px solid rgba(255,255,255,0.3);
color: white; color: white;
@ -46,7 +46,7 @@
background: rgba(255,255,255,0.3); background: rgba(255,255,255,0.3);
} }
.container { .container {
max-width: 1400px; max-width: 600px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
} }
@ -54,11 +54,10 @@
background: white; background: white;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px;
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: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: 1fr 1fr;
gap: 15px; gap: 10px;
} }
.filter-group { .filter-group {
display: flex; display: flex;
@ -103,19 +102,14 @@
.filter-buttons button.reset:hover { .filter-buttons button.reset:hover {
background: #777; background: #777;
} }
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.chart-container { .chart-container {
background: white; background: white;
padding: 20px; padding: 20px;
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);
margin-top: 20px;
} }
.chart-container h3 { .chart-container h3 {
margin-bottom: 15px; margin-bottom: 15px;
color: #333; color: #333;
} }
@ -123,27 +117,18 @@
position: relative; position: relative;
height: 300px; height: 300px;
} }
.stats-grid { .info-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: white; background: white;
padding: 20px; padding: 15px 20px;
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);
} margin-top: 20px;
.stat-card h4 {
font-size: 14px; font-size: 14px;
color: #666; color: #666;
margin-bottom: 10px;
} }
.stat-card .value { .info-panel span {
font-size: 28px; font-weight: 600;
font-weight: bold; color: #333;
color: #667eea;
} }
.error { .error {
background: #fee; background: #fee;
@ -157,72 +142,51 @@
<body> <body>
<div class="header"> <div class="header">
<div class="header-content"> <div class="header-content">
<h1>📊 Process Monitor Dashboard</h1> <h1>Process Monitor Dashboard</h1>
<p>Real-time monitoring of processes across machines</p>
</div> </div>
<button class="logout-btn" onclick="logout()">Odhlášení</button>
</div> </div>
<div class="container"> <div class="container">
<div class="filters"> <div class="filters">
<div class="filter-group" style="grid-column: 1;"> <div class="filter-group">
<label for="machine">Stroj:</label> <label for="machine">Stroj:</label>
<select id="machine"> <select id="machine">
<option value="">-- Všechny stroje --</option> <option value="">-- Všechny stroje --</option>
</select> </select>
</div> </div>
<div class="filter-group" style="grid-column: 2;"> <div class="filter-group">
<label for="process">Proces:</label> <label for="process">Proces:</label>
<select id="process"> <select id="process">
<option value="">-- Všechny procesy --</option> <option value="">-- Všechny procesy --</option>
</select> </select>
</div> </div>
<div class="filter-group" style="grid-column: 3;"> <div class="filter-group">
<label for="status">Stav:</label> <label for="status">Stav:</label>
<select id="status"> <select id="status">
<option value="">-- Všechny stavy --</option> <option value="">-- Všechny stavy --</option>
</select> </select>
</div> </div>
<div class="filter-group" style="grid-column: 4;"> <div class="filter-group">
<label for="selectedDate">Den:</label> <label for="selectedDate">Den:</label>
<input type="date" id="selectedDate"> <input type="date" id="selectedDate">
</div> </div>
<div class="filter-buttons"> <div class="filter-buttons" style="grid-column: 1 / -1;">
<button onclick="applyFilters()">Použít filtry</button> <button onclick="applyFilters()">Použít filtry</button>
<button class="reset" onclick="resetFilters()">Reset</button> <button class="reset" onclick="resetFilters()">Reset</button>
</div> </div>
</div> </div>
<div id="error"></div> <div class="info-panel">
Poslední záznam: <span id="lastRecordTime">Načítání...</span>
<div class="stats-grid">
<div class="stat-card">
<h4>Celkem záznamů</h4>
<div class="value" id="statTotal">-</div>
</div>
<div class="stat-card">
<h4>Procesy UP</h4>
<div class="value" id="statUp">-</div>
</div>
<div class="stat-card">
<h4>Procesy DOWN</h4>
<div class="value" id="statDown">-</div>
</div>
<div class="stat-card">
<h4>Dostupnost</h4>
<div class="value" id="statAvailability">-</div>
</div>
</div> </div>
<div class="charts-grid"> <div class="chart-container">
<div class="chart-container"> <h3>Doba procesů</h3>
<h3>Doba procesů vs. čas bez nalezených procesů</h3> <div class="chart-wrapper">
<div class="chart-wrapper"> <canvas id="processTimeChart"></canvas>
<canvas id="processTimeChart"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
let processTimeChart; let processTimeChart;
@ -308,15 +272,7 @@
} }
function updateStats(data) { function updateStats(data) {
const total = data.records.length; // Stats cards removed - function kept for compatibility
const up = data.records.filter(r => normalizeStatus(r.status) === 'UP').length;
const down = data.records.filter(r => normalizeStatus(r.status) === 'DOWN').length;
const availability = total > 0 ? ((up / total) * 100).toFixed(1) : 0;
document.getElementById('statTotal').textContent = total;
document.getElementById('statUp').textContent = up;
document.getElementById('statDown').textContent = down;
document.getElementById('statAvailability').textContent = availability + '%';
} }
function updateCharts(data) { function updateCharts(data) {
@ -330,19 +286,19 @@
new Date(a.detected_at) - new Date(b.detected_at) new Date(a.detected_at) - new Date(b.detected_at)
); );
// Seskupit podle procesu a spočítat časy // Seskupit podle hlavního názvu a spočítat časy
const processByName = {}; const processByName = {};
sortedRecords.forEach(r => { sortedRecords.forEach(r => {
const processName = r.process_name || '(bez procesu)'; const mainName = r.main_name || r.process_name || '(bez procesu)';
if (!processByName[processName]) { if (!processByName[mainName]) {
processByName[processName] = []; processByName[mainName] = [];
} }
processByName[processName].push(r); processByName[mainName].push(r);
}); });
// Spočítat čas běhu pro každý proces // Spočítat čas běhu pro každý hlavní název
Object.keys(processByName).forEach(processName => { Object.keys(processByName).forEach(mainName => {
const records = processByName[processName]; const records = processByName[mainName];
let totalTimeMs = 0; let totalTimeMs = 0;
for (let i = 0; i < records.length - 1; i++) { for (let i = 0; i < records.length - 1; i++) {
@ -353,19 +309,19 @@
// Převést na minuty // Převést na minuty
const totalMinutes = Math.round(totalTimeMs / 60000); const totalMinutes = Math.round(totalTimeMs / 60000);
processTimeMap[processName] = totalMinutes; processTimeMap[mainName] = totalMinutes;
}); });
if (processTimeChart) processTimeChart.destroy(); if (processTimeChart) processTimeChart.destroy();
const processTimeCtx = document.getElementById('processTimeChart').getContext('2d'); const processTimeCtx = document.getElementById('processTimeChart').getContext('2d');
// Příprava labels s časy // Příprava labels s časy
const labels = Object.keys(processTimeMap).map(processName => { const labels = Object.keys(processTimeMap).map(mainName => {
const minutes = processTimeMap[processName]; const minutes = processTimeMap[mainName];
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
const mins = minutes % 60; const mins = minutes % 60;
const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
return `${processName} (${timeStr})`; return `${mainName} (${timeStr})`;
}); });
processTimeChart = new Chart(processTimeCtx, { processTimeChart = new Chart(processTimeCtx, {
@ -415,14 +371,8 @@
applyFilters(); applyFilters();
} }
function showError(message) { function showError(message) {}
document.getElementById('error').innerHTML = function clearError() {}
'<div class="error">' + message + '</div>';
}
function clearError() {
document.getElementById('error').innerHTML = '';
}
function logout() { function logout() {
window.location.href = '/'; window.location.href = '/';
@ -430,6 +380,27 @@
window.addEventListener('load', loadFilters); window.addEventListener('load', loadFilters);
document.getElementById('selectedDate')?.addEventListener('change', applyFilters); document.getElementById('selectedDate')?.addEventListener('change', applyFilters);
async function loadLastRecordTime() {
try {
const response = await axios.get('/hb/api/data?type=lastRecordTime&apiKey=' + encodeURIComponent(apiKey));
const data = response.data;
if (data.lastRecordTime) {
const date = new Date(data.lastRecordTime);
const timeStr = date.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
document.getElementById('lastRecordTime').textContent = 'Poslední záznam: ' + timeStr;
} else {
document.getElementById('lastRecordTime').textContent = 'V databázi nejsou žádné záznamy';
}
} catch (error) {
// ignore errors for this info
}
}
// Load last record time on page load
loadLastRecordTime();
// Refresh last record time every minute
setInterval(loadLastRecordTime, 60000);
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Process Monitor - Chyba</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-container {
background: white;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
width: 100%;
max-width: 400px;
text-align: center;
}
h1 {
color: #c33;
margin-bottom: 15px;
font-size: 20px;
}
p {
color: #666;
margin-bottom: 25px;
font-size: 14px;
}
a {
display: inline-block;
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 4px;
font-weight: 600;
}
a:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<div class="error-container">
<h1>⚠️ Chyba</h1>
<p>%MESSAGE%</p>
<a href="/hb/dashboard">Zpět na přihlášení</a>
</div>
</body>
</html>

View File

@ -1,101 +0,0 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Process Monitor - Login</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px;
width: 100%;
max-width: 400px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 24px;
}
p {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
color: #333;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
</style>
</head>
<body>
<div class="login-container">
<h1>📊 Process Monitor</h1>
<p>Zadejte API klíč pro přístup na dashboard</p>
<form onsubmit="submitLogin(event)">
<div class="form-group">
<label for="apiKey">API Klíč:</label>
<input type="password" id="apiKey" name="apiKey" required autofocus>
</div>
<button type="submit">Přihlásit</button>
</form>
</div>
<script>
function submitLogin(event) {
event.preventDefault();
const apiKey = document.getElementById('apiKey').value;
window.location.href = '/hb/dashboard?apiKey=' + encodeURIComponent(apiKey);
}
</script>
</body>
</html>