fixed dashboard
This commit is contained in:
parent
352bd92885
commit
c1dcfc5a37
5
java-api/META-INF/MANIFEST.MF
Normal file
5
java-api/META-INF/MANIFEST.MF
Normal file
@ -0,0 +1,5 @@
|
||||
Manifest-Version: 1.0
|
||||
Created-By: Maven JAR Plugin 3.4.1
|
||||
Build-Jdk-Spec: 21
|
||||
Main-Class: cz.kamma.processmonitor.Main
|
||||
|
||||
475
java-api/dashboard.html
Normal file
475
java-api/dashboard.html
Normal file
@ -0,0 +1,475 @@
|
||||
<!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%';
|
||||
|
||||
function normalizeStatus(status) {
|
||||
const normalized = status.toUpperCase();
|
||||
if (normalized === 'UP' || normalized === 'RUNNING' || normalized === 'ACTIVE' || normalized === 'OK') {
|
||||
return 'UP';
|
||||
} else if (normalized === 'DOWN' || normalized === 'STOPPED' || normalized === 'INACTIVE' || normalized === 'ERROR' || normalized === 'FAILED') {
|
||||
return 'DOWN';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
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 => 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) {
|
||||
const records = data.records;
|
||||
|
||||
const statusCounts = {};
|
||||
records.forEach(r => {
|
||||
const normalized = normalizeStatus(r.status);
|
||||
statusCounts[normalized] = (statusCounts[normalized] || 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 => {
|
||||
const normalized = normalizeStatus(r.status);
|
||||
if (!statusByMachine[r.machine_name]) {
|
||||
statusByMachine[r.machine_name] = { UP: 0, DOWN: 0 };
|
||||
}
|
||||
if (normalized === 'UP') {
|
||||
statusByMachine[r.machine_name].UP++;
|
||||
} else if (normalized === 'DOWN') {
|
||||
statusByMachine[r.machine_name].DOWN++;
|
||||
}
|
||||
});
|
||||
|
||||
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 (normalizeStatus(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>
|
||||
@ -6,8 +6,8 @@
|
||||
<artifactId>process-monitor-api</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<properties>
|
||||
<maven.compiler.source>15</maven.compiler.source>
|
||||
<maven.compiler.target>15</maven.compiler.target>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
|
||||
@ -5,5 +5,5 @@ dashboard.apiKey=652f9h56gf32659twitf
|
||||
|
||||
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.user=processmon
|
||||
db.password=process621420mon
|
||||
|
||||
@ -244,6 +244,16 @@
|
||||
let statusChart, machineChart, timelineChart;
|
||||
const apiKey = '%API_KEY%';
|
||||
|
||||
function normalizeStatus(status) {
|
||||
const normalized = status.toUpperCase();
|
||||
if (normalized === 'UP' || normalized === 'RUNNING' || normalized === 'ACTIVE' || normalized === 'OK') {
|
||||
return 'UP';
|
||||
} else if (normalized === 'DOWN' || normalized === 'STOPPED' || normalized === 'INACTIVE' || normalized === 'ERROR' || normalized === 'FAILED') {
|
||||
return 'DOWN';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function loadFilters() {
|
||||
try {
|
||||
const response = await axios.get('/api/data?type=filters&apiKey=' + encodeURIComponent(apiKey));
|
||||
@ -314,8 +324,8 @@
|
||||
|
||||
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 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;
|
||||
@ -329,7 +339,8 @@
|
||||
|
||||
const statusCounts = {};
|
||||
records.forEach(r => {
|
||||
statusCounts[r.status] = (statusCounts[r.status] || 0) + 1;
|
||||
const normalized = normalizeStatus(r.status);
|
||||
statusCounts[normalized] = (statusCounts[normalized] || 0) + 1;
|
||||
});
|
||||
|
||||
if (statusChart) statusChart.destroy();
|
||||
@ -356,10 +367,15 @@
|
||||
|
||||
const statusByMachine = {};
|
||||
records.forEach(r => {
|
||||
const normalized = normalizeStatus(r.status);
|
||||
if (!statusByMachine[r.machine_name]) {
|
||||
statusByMachine[r.machine_name] = { UP: 0, DOWN: 0 };
|
||||
}
|
||||
statusByMachine[r.machine_name][r.status]++;
|
||||
if (normalized === 'UP') {
|
||||
statusByMachine[r.machine_name].UP++;
|
||||
} else if (normalized === 'DOWN') {
|
||||
statusByMachine[r.machine_name].DOWN++;
|
||||
}
|
||||
});
|
||||
|
||||
if (machineChart) machineChart.destroy();
|
||||
@ -401,7 +417,7 @@
|
||||
const upCounts = [];
|
||||
let upCount = 0;
|
||||
sortedRecords.forEach(r => {
|
||||
if (r.status === 'UP') upCount++;
|
||||
if (normalizeStatus(r.status) === 'UP') upCount++;
|
||||
upCounts.push(upCount);
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user