407 lines
15 KiB
HTML
407 lines
15 KiB
HTML
<!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: 5px;
|
|
}
|
|
.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: 600px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
.filters {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
}
|
|
.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;
|
|
}
|
|
.chart-container {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
margin-top: 20px;
|
|
}
|
|
.chart-container h3 {
|
|
margin-bottom: 15px;
|
|
color: #333;
|
|
}
|
|
.chart-wrapper {
|
|
position: relative;
|
|
height: 300px;
|
|
}
|
|
.info-panel {
|
|
background: white;
|
|
padding: 15px 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
margin-top: 20px;
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
.info-panel span {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
.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>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="filters">
|
|
<div class="filter-group">
|
|
<label for="machine">Stroj:</label>
|
|
<select id="machine">
|
|
<option value="">-- Všechny stroje --</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="process">Proces:</label>
|
|
<select id="process">
|
|
<option value="">-- Všechny procesy --</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="status">Stav:</label>
|
|
<select id="status">
|
|
<option value="">-- Všechny stavy --</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="selectedDate">Den:</label>
|
|
<input type="date" id="selectedDate">
|
|
</div>
|
|
<div class="filter-buttons" style="grid-column: 1 / -1;">
|
|
<button onclick="applyFilters()">Použít filtry</button>
|
|
<button class="reset" onclick="resetFilters()">Reset</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-panel">
|
|
Poslední záznam: <span id="lastRecordTime">Načítání...</span>
|
|
</div>
|
|
|
|
<div class="chart-container">
|
|
<h3>Doba procesů</h3>
|
|
<div class="chart-wrapper">
|
|
<canvas id="processTimeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let processTimeChart;
|
|
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('/hb/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();
|
|
document.getElementById('selectedDate').value = now.toISOString().slice(0, 10);
|
|
|
|
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 selectedDate = document.getElementById('selectedDate').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 (selectedDate) {
|
|
const from = selectedDate + 'T00:00:00Z';
|
|
const to = selectedDate + 'T23:59:59Z';
|
|
params.append('from', from);
|
|
params.append('to', to);
|
|
}
|
|
|
|
const response = await axios.get('/hb/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) {
|
|
// Stats cards removed - function kept for compatibility
|
|
}
|
|
|
|
function updateCharts(data) {
|
|
const records = data.records;
|
|
|
|
// Agregace času podle procesů
|
|
const processTimeMap = {};
|
|
|
|
// Seřadit záznamy podle procesu a času
|
|
const sortedRecords = records.sort((a, b) =>
|
|
new Date(a.detected_at) - new Date(b.detected_at)
|
|
);
|
|
|
|
// Seskupit podle hlavního názvu a spočítat časy
|
|
const processByName = {};
|
|
sortedRecords.forEach(r => {
|
|
const mainName = r.main_name || r.process_name || '(bez procesu)';
|
|
if (!processByName[mainName]) {
|
|
processByName[mainName] = [];
|
|
}
|
|
processByName[mainName].push(r);
|
|
});
|
|
|
|
// Spočítat čas běhu pro každý hlavní název
|
|
Object.keys(processByName).forEach(mainName => {
|
|
const records = processByName[mainName];
|
|
let totalTimeMs = 0;
|
|
|
|
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();
|
|
const processTimeCtx = document.getElementById('processTimeChart').getContext('2d');
|
|
|
|
// Příprava labels s časy
|
|
const labels = Object.keys(processTimeMap).map(mainName => {
|
|
const minutes = processTimeMap[mainName];
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
return `${mainName} (${timeStr})`;
|
|
});
|
|
|
|
processTimeChart = new Chart(processTimeCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
data: Object.values(processTimeMap),
|
|
backgroundColor: [
|
|
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8',
|
|
'#F7DC6F', '#BB8FCE', '#85C1E2', '#F8B88B', '#ABEBC6'
|
|
],
|
|
borderColor: '#fff',
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'bottom' },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const minutes = context.parsed;
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
if (hours > 0) {
|
|
return `${hours}h ${mins}m`;
|
|
} else {
|
|
return `${mins}m`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function resetFilters() {
|
|
document.getElementById('machine').value = '';
|
|
document.getElementById('process').value = '';
|
|
document.getElementById('status').value = '';
|
|
const now = new Date();
|
|
document.getElementById('selectedDate').value = now.toISOString().slice(0, 10);
|
|
applyFilters();
|
|
}
|
|
|
|
function showError(message) {}
|
|
function clearError() {}
|
|
|
|
function logout() {
|
|
window.location.href = '/';
|
|
}
|
|
|
|
window.addEventListener('load', loadFilters);
|
|
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>
|
|
</body>
|
|
</html>
|