added dashboard
This commit is contained in:
parent
a125de8c4a
commit
352bd92885
@ -6,8 +6,8 @@
|
|||||||
<artifactId>process-monitor-api</artifactId>
|
<artifactId>process-monitor-api</artifactId>
|
||||||
<version>0.1.0</version>
|
<version>0.1.0</version>
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>15</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>15</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|||||||
@ -31,10 +31,14 @@ public class Main {
|
|||||||
Database database = new Database(config);
|
Database database = new Database(config);
|
||||||
HttpServer server = HttpServer.create(new InetSocketAddress(config.listenHost, config.listenPort), 0);
|
HttpServer server = HttpServer.create(new InetSocketAddress(config.listenHost, config.listenPort), 0);
|
||||||
server.createContext(config.apiPath, new HeartbeatHandler(database));
|
server.createContext(config.apiPath, new HeartbeatHandler(database));
|
||||||
|
server.createContext("/", new DashboardHandler(config));
|
||||||
|
server.createContext("/api/data", new DataApiHandler(database, config));
|
||||||
server.setExecutor(null);
|
server.setExecutor(null);
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
log("Listening on http://" + config.listenHost + ":" + config.listenPort + config.apiPath);
|
log("Listening on http://" + config.listenHost + ":" + config.listenPort);
|
||||||
|
log("Dashboard: http://" + config.listenHost + ":" + config.listenPort + "/");
|
||||||
|
log("API endpoint: " + config.apiPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class HeartbeatHandler implements HttpHandler {
|
private static final class HeartbeatHandler implements HttpHandler {
|
||||||
@ -96,14 +100,16 @@ public class Main {
|
|||||||
private final String jdbcUrl;
|
private final String jdbcUrl;
|
||||||
private final String dbUser;
|
private final String dbUser;
|
||||||
private final String dbPassword;
|
private final String dbPassword;
|
||||||
|
private final String dashboardApiKey;
|
||||||
|
|
||||||
private AppConfig(String listenHost, int listenPort, String apiPath, String jdbcUrl, String dbUser, String dbPassword) {
|
private AppConfig(String listenHost, int listenPort, String apiPath, String jdbcUrl, String dbUser, String dbPassword, String dashboardApiKey) {
|
||||||
this.listenHost = listenHost;
|
this.listenHost = listenHost;
|
||||||
this.listenPort = listenPort;
|
this.listenPort = listenPort;
|
||||||
this.apiPath = apiPath;
|
this.apiPath = apiPath;
|
||||||
this.jdbcUrl = jdbcUrl;
|
this.jdbcUrl = jdbcUrl;
|
||||||
this.dbUser = dbUser;
|
this.dbUser = dbUser;
|
||||||
this.dbPassword = dbPassword;
|
this.dbPassword = dbPassword;
|
||||||
|
this.dashboardApiKey = dashboardApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AppConfig load() throws IOException {
|
private static AppConfig load() throws IOException {
|
||||||
@ -121,7 +127,8 @@ public class Main {
|
|||||||
String jdbcUrl = required(properties, "db.url");
|
String jdbcUrl = required(properties, "db.url");
|
||||||
String dbUser = required(properties, "db.user");
|
String dbUser = required(properties, "db.user");
|
||||||
String dbPassword = required(properties, "db.password");
|
String dbPassword = required(properties, "db.password");
|
||||||
return new AppConfig(listenHost, listenPort, apiPath, jdbcUrl, dbUser, dbPassword);
|
String dashboardApiKey = required(properties, "dashboard.apiKey");
|
||||||
|
return new AppConfig(listenHost, listenPort, apiPath, jdbcUrl, dbUser, dbPassword, dashboardApiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String required(Properties properties, String key) {
|
private static String required(Properties properties, String key) {
|
||||||
@ -157,6 +164,127 @@ public class Main {
|
|||||||
return inserted;
|
return inserted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FilterOptions getFilterOptions() throws SQLException {
|
||||||
|
String sqlMachines = "SELECT DISTINCT machine_name FROM process_heartbeat ORDER BY machine_name";
|
||||||
|
String sqlProcesses = "SELECT DISTINCT process_name FROM process_heartbeat WHERE process_name IS NOT NULL ORDER BY process_name";
|
||||||
|
String sqlStatuses = "SELECT DISTINCT status FROM process_heartbeat ORDER BY status";
|
||||||
|
|
||||||
|
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword)) {
|
||||||
|
List<String> machines = new java.util.ArrayList<>();
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sqlMachines);
|
||||||
|
java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
machines.add(rs.getString(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> processes = new java.util.ArrayList<>();
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sqlProcesses);
|
||||||
|
java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
processes.add(rs.getString(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> statuses = new java.util.ArrayList<>();
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sqlStatuses);
|
||||||
|
java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
statuses.add(rs.getString(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterOptions(machines, processes, statuses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
java.util.List<String> params = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
if (machine != null && !machine.isBlank()) {
|
||||||
|
sql.append(" AND machine_name = ?");
|
||||||
|
params.add(machine);
|
||||||
|
}
|
||||||
|
if (process != null && !process.isBlank()) {
|
||||||
|
sql.append(" AND process_name = ?");
|
||||||
|
params.add(process);
|
||||||
|
}
|
||||||
|
if (status != null && !status.isBlank()) {
|
||||||
|
sql.append(" AND status = ?");
|
||||||
|
params.add(status);
|
||||||
|
}
|
||||||
|
if (from != null && !from.isBlank()) {
|
||||||
|
sql.append(" AND detected_at >= ?");
|
||||||
|
params.add(from);
|
||||||
|
}
|
||||||
|
if (to != null && !to.isBlank()) {
|
||||||
|
sql.append(" AND detected_at <= ?");
|
||||||
|
params.add(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.append(" ORDER BY detected_at DESC LIMIT 10000");
|
||||||
|
|
||||||
|
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword);
|
||||||
|
PreparedStatement stmt = connection.prepareStatement(sql.toString())) {
|
||||||
|
|
||||||
|
for (int i = 0; i < params.size(); i++) {
|
||||||
|
stmt.setString(i + 1, params.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.List<Record> records = new java.util.ArrayList<>();
|
||||||
|
try (java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
records.add(new Record(
|
||||||
|
rs.getLong(1),
|
||||||
|
rs.getString(2),
|
||||||
|
rs.getString(3),
|
||||||
|
rs.getTimestamp(4),
|
||||||
|
rs.getString(5)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StatsResponse(records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FilterOptions {
|
||||||
|
List<String> machines;
|
||||||
|
List<String> processes;
|
||||||
|
List<String> statuses;
|
||||||
|
|
||||||
|
FilterOptions(List<String> machines, List<String> processes, List<String> statuses) {
|
||||||
|
this.machines = machines;
|
||||||
|
this.processes = processes;
|
||||||
|
this.statuses = statuses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Record {
|
||||||
|
long id;
|
||||||
|
String machine_name;
|
||||||
|
String status;
|
||||||
|
String detected_at;
|
||||||
|
String process_name;
|
||||||
|
|
||||||
|
Record(long id, String machine_name, String status, Timestamp detected_at, String process_name) {
|
||||||
|
this.id = id;
|
||||||
|
this.machine_name = machine_name;
|
||||||
|
this.status = status;
|
||||||
|
this.detected_at = detected_at != null ? detected_at.toInstant().toString() : null;
|
||||||
|
this.process_name = process_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StatsResponse {
|
||||||
|
List<Record> records;
|
||||||
|
|
||||||
|
StatsResponse(List<Record> records) {
|
||||||
|
this.records = records;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class HeartbeatRequest {
|
private static final class HeartbeatRequest {
|
||||||
@ -202,6 +330,148 @@ public class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class DashboardHandler implements HttpHandler {
|
||||||
|
private final AppConfig config;
|
||||||
|
|
||||||
|
private DashboardHandler(AppConfig config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange exchange) throws IOException {
|
||||||
|
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
|
sendHtml(exchange, 405, "<h1>Method Not Allowed</h1>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String query = exchange.getRequestURI().getQuery();
|
||||||
|
String apiKey = getParam(query, "apiKey");
|
||||||
|
|
||||||
|
if (apiKey == null || apiKey.isBlank()) {
|
||||||
|
String html = loadResource("login.html");
|
||||||
|
sendHtml(exchange, 200, html);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey.equals(config.dashboardApiKey)) {
|
||||||
|
String html = loadResource("error.html").replace("%MESSAGE%", escapeHtml("Neplatný API klíč"));
|
||||||
|
sendHtml(exchange, 401, html);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String html = loadResource("dashboard.html").replace("%API_KEY%", apiKey);
|
||||||
|
sendHtml(exchange, 200, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getParam(String query, String name) {
|
||||||
|
if (query == null) return null;
|
||||||
|
String[] pairs = query.split("&");
|
||||||
|
for (String pair : pairs) {
|
||||||
|
String[] parts = pair.split("=", 2);
|
||||||
|
if (parts.length == 2 && parts[0].equals(name)) {
|
||||||
|
try {
|
||||||
|
return java.net.URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadResource(String resourceName) throws IOException {
|
||||||
|
try (InputStream inputStream = Main.class.getClassLoader().getResourceAsStream(resourceName)) {
|
||||||
|
if (inputStream == null) {
|
||||||
|
throw new IOException("Resource not found: " + resourceName);
|
||||||
|
}
|
||||||
|
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHtml(HttpExchange exchange, int statusCode, String html) throws IOException {
|
||||||
|
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
|
||||||
|
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||||
|
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||||
|
outputStream.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DataApiHandler implements HttpHandler {
|
||||||
|
private final Database database;
|
||||||
|
private final AppConfig config;
|
||||||
|
|
||||||
|
private DataApiHandler(Database database, AppConfig config) {
|
||||||
|
this.database = database;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange exchange) throws IOException {
|
||||||
|
try {
|
||||||
|
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
|
sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String query = exchange.getRequestURI().getQuery();
|
||||||
|
String apiKey = getParam(query, "apiKey");
|
||||||
|
|
||||||
|
if (apiKey == null || apiKey.isBlank() || !apiKey.equals(config.dashboardApiKey)) {
|
||||||
|
sendJson(exchange, 401, "{\"error\":\"unauthorized\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String type = getParam(query, "type");
|
||||||
|
|
||||||
|
if ("filters".equals(type)) {
|
||||||
|
String response = GSON.toJson(database.getFilterOptions());
|
||||||
|
sendJson(exchange, 200, response);
|
||||||
|
} else if ("stats".equals(type)) {
|
||||||
|
String machine = getParam(query, "machine");
|
||||||
|
String process = getParam(query, "process");
|
||||||
|
String status = getParam(query, "status");
|
||||||
|
String from = getParam(query, "from");
|
||||||
|
String to = getParam(query, "to");
|
||||||
|
|
||||||
|
String response = GSON.toJson(database.getStats(machine, process, status, from, to));
|
||||||
|
sendJson(exchange, 200, response);
|
||||||
|
} else {
|
||||||
|
sendJson(exchange, 400, "{\"error\":\"invalid_type\"}");
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
sendJson(exchange, 500, "{\"error\":\"internal_error\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getParam(String query, String name) {
|
||||||
|
if (query == null) return null;
|
||||||
|
String[] pairs = query.split("&");
|
||||||
|
for (String pair : pairs) {
|
||||||
|
String[] parts = pair.split("=", 2);
|
||||||
|
if (parts.length == 2 && parts[0].equals(name)) {
|
||||||
|
try {
|
||||||
|
return java.net.URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
|
||||||
|
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||||
|
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||||
|
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||||
|
outputStream.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void log(String message) {
|
private static void log(String message) {
|
||||||
System.out.printf("[%s] %s%n", Instant.now(), message);
|
System.out.printf("[%s] %s%n", Instant.now(), message);
|
||||||
}
|
}
|
||||||
@ -209,4 +479,8 @@ public class Main {
|
|||||||
private static String escapeJson(String value) {
|
private static String escapeJson(String value) {
|
||||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String escapeHtml(String value) {
|
||||||
|
return value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
server.host=0.0.0.0
|
server.host=0.0.0.0
|
||||||
server.port=80
|
server.port=8080
|
||||||
server.path=/hb/api
|
server.path=/hb/api
|
||||||
|
dashboard.apiKey=652f9h56gf32659twitf
|
||||||
|
|
||||||
db.url=jdbc:mariadb://127.0.0.1:3306/process_monitor?useUnicode=true&characterEncoding=utf8
|
db.url=jdbc:mariadb://10.0.0.147:3306/process_monitor?useUnicode=true&characterEncoding=utf8
|
||||||
# Change these credentials before running.
|
# Change these credentials before running.
|
||||||
db.user=process_monitor
|
db.user=process_monitor
|
||||||
db.password=process_monitor_secret
|
db.password=process_monitor_secret
|
||||||
|
|||||||
459
java-api/src/main/resources/dashboard.html
Normal file
459
java-api/src/main/resources/dashboard.html
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Process Monitor - Dashboard</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.logout-btn {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.filters {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.filter-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.filter-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.filter-group select,
|
||||||
|
.filter-group input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.filter-group input {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.filter-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.filter-buttons button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.filter-buttons button:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
.filter-buttons button.reset {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
.filter-buttons button.reset:hover {
|
||||||
|
background: #777;
|
||||||
|
}
|
||||||
|
.charts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.chart-container {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.chart-container h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.stat-card h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background: #fee;
|
||||||
|
color: #c33;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>📊 Process Monitor Dashboard</h1>
|
||||||
|
<p>Real-time monitoring of processes across machines</p>
|
||||||
|
</div>
|
||||||
|
<button class="logout-btn" onclick="logout()">Odhlášení</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="filters">
|
||||||
|
<div class="filter-group" style="grid-column: 1;">
|
||||||
|
<label for="machine">Stroj:</label>
|
||||||
|
<select id="machine">
|
||||||
|
<option value="">-- Všechny stroje --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group" style="grid-column: 2;">
|
||||||
|
<label for="process">Proces:</label>
|
||||||
|
<select id="process">
|
||||||
|
<option value="">-- Všechny procesy --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group" style="grid-column: 3;">
|
||||||
|
<label for="status">Stav:</label>
|
||||||
|
<select id="status">
|
||||||
|
<option value="">-- Všechny stavy --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group" style="grid-column: 4;">
|
||||||
|
<label for="dateFrom">Od:</label>
|
||||||
|
<input type="datetime-local" id="dateFrom">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group" style="grid-column: 5;">
|
||||||
|
<label for="dateTo">Do:</label>
|
||||||
|
<input type="datetime-local" id="dateTo">
|
||||||
|
</div>
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<button onclick="applyFilters()">Použít filtry</button>
|
||||||
|
<button class="reset" onclick="resetFilters()">Reset</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="error"></div>
|
||||||
|
|
||||||
|
<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 class="charts-grid">
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>Stav procesů</h3>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="statusChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>Stavy podle strojů</h3>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="machineChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container" style="grid-column: 1 / -1;">
|
||||||
|
<h3>Dostupnost v čase</h3>
|
||||||
|
<div class="chart-wrapper" style="height: 400px;">
|
||||||
|
<canvas id="timelineChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let statusChart, machineChart, timelineChart;
|
||||||
|
const apiKey = '%API_KEY%';
|
||||||
|
|
||||||
|
async function loadFilters() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/data?type=filters&apiKey=' + encodeURIComponent(apiKey));
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
const machineSelect = document.getElementById('machine');
|
||||||
|
data.machines.forEach(m => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = m;
|
||||||
|
option.textContent = m;
|
||||||
|
machineSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const processSelect = document.getElementById('process');
|
||||||
|
data.processes.forEach(p => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = p;
|
||||||
|
option.textContent = p || '(bez procesu)';
|
||||||
|
processSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusSelect = document.getElementById('status');
|
||||||
|
data.statuses.forEach(s => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = s;
|
||||||
|
option.textContent = s;
|
||||||
|
statusSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
document.getElementById('dateTo').value = now.toISOString().slice(0, 16);
|
||||||
|
document.getElementById('dateFrom').value = sevenDaysAgo.toISOString().slice(0, 16);
|
||||||
|
|
||||||
|
applyFilters();
|
||||||
|
} catch (error) {
|
||||||
|
showError('Chyba při načítání filtrů: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyFilters() {
|
||||||
|
try {
|
||||||
|
clearError();
|
||||||
|
const machine = document.getElementById('machine').value;
|
||||||
|
const process = document.getElementById('process').value;
|
||||||
|
const status = document.getElementById('status').value;
|
||||||
|
const dateFrom = document.getElementById('dateFrom').value;
|
||||||
|
const dateTo = document.getElementById('dateTo').value;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('type', 'stats');
|
||||||
|
params.append('apiKey', apiKey);
|
||||||
|
if (machine) params.append('machine', machine);
|
||||||
|
if (process) params.append('process', process);
|
||||||
|
if (status) params.append('status', status);
|
||||||
|
if (dateFrom) params.append('from', new Date(dateFrom).toISOString());
|
||||||
|
if (dateTo) params.append('to', new Date(dateTo).toISOString());
|
||||||
|
|
||||||
|
const response = await axios.get('/api/data?' + params.toString());
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
updateStats(data);
|
||||||
|
updateCharts(data);
|
||||||
|
} catch (error) {
|
||||||
|
showError('Chyba při načítání dat: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats(data) {
|
||||||
|
const total = data.records.length;
|
||||||
|
const up = data.records.filter(r => r.status === 'UP').length;
|
||||||
|
const down = data.records.filter(r => 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) {
|
||||||
|
const records = data.records;
|
||||||
|
|
||||||
|
const statusCounts = {};
|
||||||
|
records.forEach(r => {
|
||||||
|
statusCounts[r.status] = (statusCounts[r.status] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (statusChart) statusChart.destroy();
|
||||||
|
const statusCtx = document.getElementById('statusChart').getContext('2d');
|
||||||
|
statusChart = new Chart(statusCtx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: Object.keys(statusCounts),
|
||||||
|
datasets: [{
|
||||||
|
data: Object.values(statusCounts),
|
||||||
|
backgroundColor: ['#4CAF50', '#FF6B6B'],
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'bottom' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusByMachine = {};
|
||||||
|
records.forEach(r => {
|
||||||
|
if (!statusByMachine[r.machine_name]) {
|
||||||
|
statusByMachine[r.machine_name] = { UP: 0, DOWN: 0 };
|
||||||
|
}
|
||||||
|
statusByMachine[r.machine_name][r.status]++;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (machineChart) machineChart.destroy();
|
||||||
|
const machineCtx = document.getElementById('machineChart').getContext('2d');
|
||||||
|
machineChart = new Chart(machineCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: Object.keys(statusByMachine),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'UP',
|
||||||
|
backgroundColor: '#4CAF50',
|
||||||
|
data: Object.values(statusByMachine).map(s => s.UP)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'DOWN',
|
||||||
|
backgroundColor: '#FF6B6B',
|
||||||
|
data: Object.values(statusByMachine).map(s => s.DOWN)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: { stacked: true },
|
||||||
|
y: { stacked: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedRecords = records.sort((a, b) =>
|
||||||
|
new Date(a.detected_at) - new Date(b.detected_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
const timeLabels = sortedRecords.map(r =>
|
||||||
|
new Date(r.detected_at).toLocaleString('cs-CZ')
|
||||||
|
);
|
||||||
|
const upCounts = [];
|
||||||
|
let upCount = 0;
|
||||||
|
sortedRecords.forEach(r => {
|
||||||
|
if (r.status === 'UP') upCount++;
|
||||||
|
upCounts.push(upCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timelineChart) timelineChart.destroy();
|
||||||
|
const timelineCtx = document.getElementById('timelineChart').getContext('2d');
|
||||||
|
timelineChart = new Chart(timelineCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Procesy UP',
|
||||||
|
borderColor: '#4CAF50',
|
||||||
|
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
||||||
|
data: upCounts,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'bottom' }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: { beginAtZero: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
document.getElementById('machine').value = '';
|
||||||
|
document.getElementById('process').value = '';
|
||||||
|
document.getElementById('status').value = '';
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
document.getElementById('error').innerHTML =
|
||||||
|
'<div class="error">' + message + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearError() {
|
||||||
|
document.getElementById('error').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', loadFilters);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
java-api/src/main/resources/error.html
Normal file
61
java-api/src/main/resources/error.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<!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="/">Zpět na přihlášení</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
101
java-api/src/main/resources/login.html
Normal file
101
java-api/src/main/resources/login.html
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<!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 = '/?apiKey=' + encodeURIComponent(apiKey);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user