From 352bd9288559f3c4b47f5219c9ddf6277e08609c Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Fri, 27 Mar 2026 23:25:32 +0100 Subject: [PATCH] added dashboard --- java-api/pom.xml | 4 +- .../java/cz/kamma/processmonitor/Main.java | 280 ++++++++++- .../src/main/resources/application.properties | 7 +- java-api/src/main/resources/dashboard.html | 459 ++++++++++++++++++ java-api/src/main/resources/error.html | 61 +++ java-api/src/main/resources/login.html | 101 ++++ 6 files changed, 904 insertions(+), 8 deletions(-) create mode 100644 java-api/src/main/resources/dashboard.html create mode 100644 java-api/src/main/resources/error.html create mode 100644 java-api/src/main/resources/login.html diff --git a/java-api/pom.xml b/java-api/pom.xml index dfea96b..6c1aae1 100644 --- a/java-api/pom.xml +++ b/java-api/pom.xml @@ -6,8 +6,8 @@ process-monitor-api 0.1.0 - 11 - 11 + 15 + 15 UTF-8 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 1e8a74d..6bf4d29 100644 --- a/java-api/src/main/java/cz/kamma/processmonitor/Main.java +++ b/java-api/src/main/java/cz/kamma/processmonitor/Main.java @@ -31,10 +31,14 @@ public class Main { Database database = new Database(config); HttpServer server = HttpServer.create(new InetSocketAddress(config.listenHost, config.listenPort), 0); server.createContext(config.apiPath, new HeartbeatHandler(database)); + server.createContext("/", new DashboardHandler(config)); + server.createContext("/api/data", new DataApiHandler(database, config)); server.setExecutor(null); 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 { @@ -96,14 +100,16 @@ public class Main { private final String jdbcUrl; private final String dbUser; 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.listenPort = listenPort; this.apiPath = apiPath; this.jdbcUrl = jdbcUrl; this.dbUser = dbUser; this.dbPassword = dbPassword; + this.dashboardApiKey = dashboardApiKey; } private static AppConfig load() throws IOException { @@ -121,7 +127,8 @@ public class Main { String jdbcUrl = required(properties, "db.url"); String dbUser = required(properties, "db.user"); 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) { @@ -157,6 +164,127 @@ public class Main { 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 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 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 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 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 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 machines; + List processes; + List statuses; + + FilterOptions(List machines, List processes, List 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 records; + + StatsResponse(List records) { + this.records = records; + } } 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, "

Method Not Allowed

"); + 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) { System.out.printf("[%s] %s%n", Instant.now(), message); } @@ -209,4 +479,8 @@ public class Main { private static String escapeJson(String value) { return value.replace("\\", "\\\\").replace("\"", "\\\""); } + + private static String escapeHtml(String value) { + return value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'"); + } } diff --git a/java-api/src/main/resources/application.properties b/java-api/src/main/resources/application.properties index 37416a5..a666091 100644 --- a/java-api/src/main/resources/application.properties +++ b/java-api/src/main/resources/application.properties @@ -1,8 +1,9 @@ server.host=0.0.0.0 -server.port=80 +server.port=8080 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. db.user=process_monitor -db.password=process_monitor_secret +db.password=process_monitor_secret diff --git a/java-api/src/main/resources/dashboard.html b/java-api/src/main/resources/dashboard.html new file mode 100644 index 0000000..0833a08 --- /dev/null +++ b/java-api/src/main/resources/dashboard.html @@ -0,0 +1,459 @@ + + + + + + Process Monitor - Dashboard + + + + + +
+
+

📊 Process Monitor Dashboard

+

Real-time monitoring of processes across machines

+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+

Celkem záznamů

+
-
+
+
+

Procesy UP

+
-
+
+
+

Procesy DOWN

+
-
+
+
+

Dostupnost

+
-
+
+
+ +
+
+

Stav procesů

+
+ +
+
+
+

Stavy podle strojů

+
+ +
+
+
+

Dostupnost v čase

+
+ +
+
+
+
+ + + + diff --git a/java-api/src/main/resources/error.html b/java-api/src/main/resources/error.html new file mode 100644 index 0000000..7d0cc6a --- /dev/null +++ b/java-api/src/main/resources/error.html @@ -0,0 +1,61 @@ + + + + + + Process Monitor - Chyba + + + +
+

⚠️ Chyba

+

%MESSAGE%

+ Zpět na přihlášení +
+ + diff --git a/java-api/src/main/resources/login.html b/java-api/src/main/resources/login.html new file mode 100644 index 0000000..abc9bdf --- /dev/null +++ b/java-api/src/main/resources/login.html @@ -0,0 +1,101 @@ + + + + + + Process Monitor - Login + + + + + + +