poprawki i eventy

This commit is contained in:
Mateusz Gruszczyński
2026-02-04 11:53:55 +01:00
parent 0285b2c203
commit 64887698d2
8 changed files with 538 additions and 210 deletions

View File

@@ -3,200 +3,332 @@ let currentTimeRange = '6h';
let phasesConfig = {};
const gauges = {};
let voltageChart = null;
const THRESHOLDS = { min: 207, max: 253 };
/**
* Inicjalizacja monitora
*/
function initMonitor(phases, defaultRange) {
phasesConfig = phases;
currentTimeRange = defaultRange;
setupGauges();
setupMainChart();
const gaugeConfig = {
type: 'doughnut',
data: {
datasets: [{
data: [0, 100],
backgroundColor: ['#198754', '#1a1d20'],
borderWidth: 0,
circumference: 180,
rotation: 270
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
cutout: '75%',
plugins: { legend: { display: false }, tooltip: { enabled: false } }
}
};
Object.keys(phasesConfig).forEach(id => {
const canvas = document.getElementById('gauge' + id);
if (canvas) {
gauges[id] = new Chart(canvas, JSON.parse(JSON.stringify(gaugeConfig)));
updateGaugeUI(id, 230);
}
});
const ctxChart = document.getElementById('voltageChart');
if (ctxChart) {
voltageChart = new Chart(ctxChart, {
type: 'line',
data: { datasets: [] },
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time: { displayFormats: { hour: 'HH:mm', minute: 'HH:mm' }, tooltipFormat: 'HH:mm' },
grid: { color: '#2d3139' }
},
y: {
beginAtZero: false,
suggestedMin: 210,
suggestedMax: 255,
grid: { color: '#2d3139' },
ticks: { stepSize: 5, color: '#c9d1d9' }
}
},
plugins: {
legend: {
labels: {
color: '#c9d1d9',
filter: function(item) {
return !item.text.includes('Zanik') && !item.text.includes('Powrot');
}
}
},
tooltip: {
callbacks: {
label: function(context) {
if (context.dataset.label.includes('Zanik')) {
return 'ZANIK: ' + context.raw.realV.toFixed(1) + 'V';
}
if (context.dataset.label.includes('Powrot')) {
return 'POWROT: ' + context.raw.realV.toFixed(1) + 'V';
}
let label = context.dataset.label || '';
if (context.parsed.y !== null) {
label += ': ' + context.parsed.y.toFixed(1) + 'V';
}
return label;
}
}
}
}
}
});
}
socket.on('voltage_update', function(data) {
socket.on('voltage_update', (data) => {
Object.keys(phasesConfig).forEach(id => {
const val = data['phase' + id];
const textElement = document.getElementById('value' + id);
if (val !== undefined && val !== null) {
const numVal = parseFloat(val);
if (textElement) {
textElement.textContent = numVal.toFixed(1) + 'V';
textElement.style.color = numVal < 200 ? '#dc3545' : '#fff';
}
updateGaugeUI(id, numVal);
const textEl = document.getElementById('value' + id);
if (val !== undefined && val !== null && textEl) {
const num = parseFloat(val);
textEl.textContent = num.toFixed(1) + 'V';
textEl.style.color = num < 200 ? '#dc3545' : '#fff';
updateGaugeUI(id, num);
}
});
if (data.timestamp) {
const date = new Date(data.timestamp);
document.getElementById('lastUpdate').textContent = 'Odczyt: ' + date.toLocaleTimeString('pl-PL', {hour12: false});
document.getElementById('lastUpdate').textContent = 'Odczyt: ' +
new Date(data.timestamp).toLocaleTimeString('pl-PL', {hour12: false});
}
});
window.changeTimeRange(currentTimeRange);
}
function updateGaugeUI(id, val) {
if (!gauges[id]) return;
const percentage = Math.max(0, Math.min(100, ((val - 190) / 80) * 100));
let color = '#198754';
if (val < THRESHOLDS.min || val > THRESHOLDS.max) color = '#dc3545';
else if (val < 212 || val > 248) color = '#ffc107';
gauges[id].data.datasets[0].data = [percentage, 100 - percentage];
gauges[id].data.datasets[0].backgroundColor = [color, '#1a1d20'];
gauges[id].update('none');
/**
* Konfiguracja wykresu głównego (Zoom + Pan)
*/
function setupMainChart() {
const ctx = document.getElementById('voltageChart');
if (!ctx) return;
voltageChart = new Chart(ctx, {
type: 'line',
data: { datasets: [] },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
scales: {
x: {
type: 'time',
time: {
displayFormats: { hour: 'HH:mm', minute: 'HH:mm' },
tooltipFormat: 'yyyy-MM-dd HH:mm:ss'
},
grid: { color: '#2d3139' },
ticks: { color: '#8b949e' }
},
y: {
beginAtZero: false,
suggestedMin: 200,
suggestedMax: 250,
grid: { color: '#2d3139' },
ticks: { stepSize: 5, color: '#c9d1d9' }
}
},
plugins: {
zoom: {
limits: {
x: {
rangeMax: 30 * 24 * 60 * 60 * 1000
}
},
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
drag: {
enabled: true,
backgroundColor: 'rgba(54, 162, 235, 0.3)'
},
mode: 'x',
onZoomComplete: async ({chart}) => {
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
currentTimeRange = 'precise';
await reloadDataForRange(chart.scales.x.min, chart.scales.x.max);
}
},
pan: {
enabled: true,
mode: 'x',
onPanComplete: async ({chart}) => {
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
currentTimeRange = 'precise';
await reloadDataForRange(chart.scales.x.min, chart.scales.x.max);
}
}
},
legend: {
labels: {
color: '#c9d1d9',
filter: (item) => !['Zanik', 'Powrot', 'Awaria'].some(word => item.text.includes(word))
}
},
tooltip: {
enabled: true,
position: 'nearest',
callbacks: {
label: (ctx) => {
const datasetLabel = ctx.dataset.label || '';
const raw = ctx.raw;
const val = (raw && raw.realV !== undefined) ? raw.realV : ctx.parsed.y;
if (datasetLabel.includes('Zanik')) return `ZANIK FAZY`;
if (datasetLabel.includes('Powrot')) return `POWROT: ${val.toFixed(1)}V`;
if (datasetLabel.includes('Awaria')) return null;
return `${datasetLabel}: ${val.toFixed(1)}V`;
}
}
}
}
}
});
}
window.changeTimeRange = async function(range) {
/**
* Pobieranie danych i gotowych eventów z backendu
*/
async function reloadDataForRange(min, max, rangeName = null) {
let urlParams = rangeName
? `range=${rangeName}`
: `start=${new Date(min).toISOString()}&end=${new Date(max).toISOString()}`;
const newDatasets = [];
for (let id of Object.keys(phasesConfig)) {
try {
const raw = await fetch(`/api/timeseries/${id}?${urlParams}`).then(r => r.json());
const proc = processPhaseData(id, raw);
newDatasets.push(proc.lineDataset);
if (proc.outageLine) newDatasets.push(proc.outageLine);
if (proc.outageDataset) newDatasets.push(proc.outageDataset);
if (proc.recoveryDataset) newDatasets.push(proc.recoveryDataset);
} catch (e) {
console.error("Błąd pobierania fazy " + id, e);
}
}
try {
const events = await fetch(`/api/events?${urlParams}`).then(r => r.json());
renderEventLog(events, rangeName || 'precise');
} catch (e) {
console.error("Błąd pobierania zdarzeń", e);
}
voltageChart.data.datasets = newDatasets;
if (rangeName) {
voltageChart.update();
} else {
voltageChart.update('none');
}
const finalMin = rangeName ? voltageChart.scales.x.min : (min || voltageChart.scales.x.min);
const finalMax = rangeName ? voltageChart.scales.x.max : (max || voltageChart.scales.x.max);
updateRangeLabel(finalMin, finalMax);
}
/**
* Dynamiczna aktualizacja napisu zakresu czasu nad logami
*/
function updateRangeLabel(min, max) {
const label = document.getElementById('eventRangeLabel');
if (!label) return;
const start = new Date(min);
const end = new Date(max);
const optTime = { hour: '2-digit', minute: '2-digit', hour12: false };
const optDate = { day: '2-digit', month: '2-digit' };
if (start.toDateString() === end.toDateString()) {
label.textContent = `Zakres: ${start.toLocaleDateString('pl-PL', optDate)}, ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleTimeString('pl-PL', optTime)}`;
} else {
label.textContent = `Zakres: ${start.toLocaleDateString('pl-PL', optDate)} ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleDateString('pl-PL', optDate)} ${end.toLocaleTimeString('pl-PL', optTime)}`;
}
}
window.changeTimeRange = function(range) {
currentTimeRange = range;
if (voltageChart) {
voltageChart.options.scales.x.min = undefined;
voltageChart.options.scales.x.max = undefined;
}
document.querySelectorAll('.time-btn').forEach(btn => btn.classList.remove('active'));
const activeBtn = document.querySelector(`[data-range="${range}"]`);
if (activeBtn) activeBtn.classList.add('active');
if (!voltageChart) return;
const phaseIds = Object.keys(phasesConfig);
voltageChart.data.datasets = [];
for (let id of phaseIds) {
try {
const response = await fetch(`/api/timeseries/${id}?range=${range}`);
const data = await response.json();
const lineData = [];
const outagePoints = [];
const recoveryPoints = [];
let wasOutage = false;
data.forEach(p => {
const v = p.voltage;
const t = new Date(p.time);
const minY = voltageChart.scales.y.min || 190;
if (v < 200 && v !== null) {
outagePoints.push({ x: t, y: minY + 0.5, realV: v });
lineData.push({ x: t, y: null });
wasOutage = true;
} else {
if (wasOutage) {
recoveryPoints.push({ x: t, y: v, realV: v });
wasOutage = false;
}
lineData.push({ x: t, y: v });
}
});
voltageChart.data.datasets.push({
label: phasesConfig[id].label,
data: lineData,
borderColor: phasesConfig[id].color,
backgroundColor: phasesConfig[id].color + '15',
tension: 0,
borderWidth: 2,
spanGaps: false,
pointRadius: 0
});
if (outagePoints.length > 0) {
voltageChart.data.datasets.push({
label: 'Zanik ' + phasesConfig[id].label,
data: outagePoints,
type: 'scatter',
pointRadius: 5,
pointBackgroundColor: '#ff0000ff',
pointBorderColor: phasesConfig[id].color,
pointBorderWidth: 2,
z: 999
});
}
if (recoveryPoints.length > 0) {
voltageChart.data.datasets.push({
label: 'Powrot ' + phasesConfig[id].label,
data: recoveryPoints,
type: 'scatter',
pointRadius: 5,
pointBackgroundColor: '#3fb99aff',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
z: 999
});
}
} catch (e) { console.error(e); }
}
voltageChart.update();
reloadDataForRange(null, null, range);
};
/**
* Przetwarzanie danych z obsługą przerywanej linii awarii
*/
function processPhaseData(id, data) {
const lineData = [], outagePoints = [], recoveryPoints = [], outageLineData = [];
let wasOutage = false;
data.forEach(p => {
const v = p.voltage, t = new Date(p.time);
if (v < 180 && v !== null) {
const pOut = { x: t, y: 195, realV: v };
outagePoints.push(pOut);
outageLineData.push(pOut);
lineData.push({ x: t, y: null });
wasOutage = true;
} else {
if (wasOutage) {
const pRec = { x: t, y: v, realV: v };
recoveryPoints.push(pRec);
outageLineData.push(pRec);
wasOutage = false;
} else {
outageLineData.push({ x: t, y: null });
}
lineData.push({ x: t, y: v });
}
});
return {
lineDataset: {
label: phasesConfig[id].label, data: lineData, borderColor: phasesConfig[id].color,
backgroundColor: phasesConfig[id].color + '15', tension: 0.1, borderWidth: 2, spanGaps: false, pointRadius: 0
},
outageLine: {
label: 'Awaria ' + phasesConfig[id].label, data: outageLineData,
borderColor: '#ff0000', borderDash: [5, 5], borderWidth: 1,
pointRadius: 0, fill: false, spanGaps: false, showLine: true
},
outageDataset: outagePoints.length ? {
label: 'Zanik ' + phasesConfig[id].label, data: outagePoints, type: 'scatter',
pointRadius: 4, pointBackgroundColor: '#ff0000', pointBorderColor: '#fff', pointBorderWidth: 1, z: 99
} : null,
recoveryDataset: recoveryPoints.length ? {
label: 'Powrot ' + phasesConfig[id].label, data: recoveryPoints, type: 'scatter',
pointRadius: 4, pointBackgroundColor: '#3fb950', pointBorderColor: '#fff', pointBorderWidth: 1, z: 99
} : null
};
}
function renderEventLog(events, range) {
const container = document.getElementById('eventLogContainer');
if (!container) return;
container.innerHTML = '';
if (events && events.length > 0) {
events.forEach(ev => {
const start = new Date(ev.start);
const end = new Date(ev.end);
const dur = ev.duration;
const phase = phasesConfig[ev.phase];
const item = document.createElement('div');
item.className = 'event-item';
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.gap = '10px';
const dateOpt = { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false };
item.innerHTML = `
<div class="event-badge" style="background-color: ${phase.color}"></div>
<div class="event-time">${start.toLocaleString('pl-PL', dateOpt)} - ${end.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'})}</div>
<div class="event-desc" style="flex-grow: 1;">Faza <strong>${phase.label}</strong>: brak przez ${dur} min.</div>
<button class="btn btn-sm btn-outline-info show-event-btn" title="Pokaż 3h wokół zdarzenia"
style="padding: 2px 8px; font-size: 11px; height: 24px; color: #58a6ff; border-color: #30363d; min-width: 50px;">
Pokaż
</button>
`;
const btn = item.querySelector('.show-event-btn');
btn.addEventListener('click', async () => {
const eventCenter = start.getTime() + (end.getTime() - start.getTime()) / 2;
const windowMs = 3 * 60 * 60 * 1000;
const viewStart = eventCenter - (windowMs / 2);
const viewEnd = eventCenter + (windowMs / 2);
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
currentTimeRange = 'precise';
if (voltageChart) {
voltageChart.options.scales.x.min = undefined;
voltageChart.options.scales.x.max = undefined;
}
await reloadDataForRange(viewStart, viewEnd, null);
document.getElementById('voltageChart').scrollIntoView({ behavior: 'smooth' });
});
container.appendChild(item);
});
} else {
container.innerHTML = '<div class="no-events">Brak zarejestrowanych zaników.</div>';
}
}
function setupGauges() {
const config = {
type: 'doughnut',
data: { datasets: [{ data: [0, 100], backgroundColor: ['#198754', '#1a1d20'], borderWidth: 0, circumference: 180, rotation: 270 }] },
options: { responsive: true, maintainAspectRatio: true, cutout: '75%', plugins: { legend: { display: false }, tooltip: { enabled: false } } }
};
Object.keys(phasesConfig).forEach(id => {
const canvas = document.getElementById('gauge' + id);
if (canvas) gauges[id] = new Chart(canvas, JSON.parse(JSON.stringify(config)));
});
}
function updateGaugeUI(id, val) {
if (!gauges[id]) return;
const pct = Math.max(0, Math.min(100, ((val - 190) / 80) * 100));
let color = (val < THRESHOLDS.min || val > THRESHOLDS.max) ? '#dc3545' : ((val < 212 || val > 248) ? '#ffc107' : '#198754');
gauges[id].data.datasets[0].data = [pct, 100 - pct];
gauges[id].data.datasets[0].backgroundColor = [color, '#1a1d20'];
gauges[id].update('none');
}