From 0910b4ddb4c1e760080943ad950f453247ba969a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Thu, 5 Feb 2026 09:06:01 +0100 Subject: [PATCH] poprawki i optymalizacje kodu --- app.py | 4 +- static/css/style.css | 136 +++++++++++++++++++++++++++++++++++++++- static/js/gauge.js | 20 ++++++ static/js/modal.js | 96 ++++++++++++++++++++++++++++ static/js/monitor.js | 145 +++++++++++++++++++++++++++---------------- templates/base.html | 11 +++- templates/index.html | 32 +++++++++- 7 files changed, 381 insertions(+), 63 deletions(-) create mode 100644 static/js/gauge.js create mode 100644 static/js/modal.js diff --git a/app.py b/app.py index 9062719..e0ccead 100644 --- a/app.py +++ b/app.py @@ -80,7 +80,7 @@ def add_header(response): else: response.cache_control.no_cache = True response.cache_control.no_store = True - response.cache_control.must_revalidate = True + #response.cache_control.must_revalidate = True return response @app.route('/favicon.ico') @@ -157,7 +157,7 @@ def get_events(): try: days = int(''.join(filter(str.isdigit, range_p))) if days > 61: - return jsonify({"error": "range_too_large", "message": "Zbyt duży zakres. Zaznacz obszar na wykresie."}) + return jsonify({"error": "range_too_large", "message": "Zbyt duży zakres. Zaznacz obszar na wykresie lub wybierz własny zakres."}) except: pass if start_p and end_p: diff --git a/static/css/style.css b/static/css/style.css index 25b27fc..d6059d9 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -208,4 +208,138 @@ body { .border-dotted { border-style: dotted !important; -} \ No newline at end of file +} + +/* Modal własnego zakresu */ +#customRangeModal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.modal-content { + background: var(--card-bg); + padding: 25px; + border-radius: 12px; + max-width: 420px; + width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + border: 1px solid var(--border-color); +} + +.modal-content h3 { + margin-top: 0; + color: var(--text-main); + margin-bottom: 20px; + font-size: 1.25rem; + font-weight: 600; +} + +.modal-form-group { + margin-bottom: 15px; +} + +.modal-form-group:last-of-type { + margin-bottom: 25px; +} + +.modal-label { + display: block; + color: #8b949e; + margin-bottom: 8px; + font-size: 0.875rem; + font-weight: 500; +} + +.modal-input { + width: 100%; + padding: 10px; + background: var(--bg-dark); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-main); + font-size: 14px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; + box-sizing: border-box; +} + +.modal-input:focus { + outline: none; + border-color: var(--blue-accent); +} + +.modal-buttons { + display: flex; + gap: 10px; + justify-content: flex-end; +} + +.modal-btn { + padding: 8px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s; + border: none; +} + +.modal-btn-cancel { + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-main); +} + +.modal-btn-cancel:hover { + background: rgba(48, 54, 61, 0.5); +} + +.modal-btn-apply { + background: #1f6feb; + border: 1px solid #1f6feb; + color: #ffffff; + font-weight: 600; +} + +.modal-btn-apply:hover { + background: #1a5acc; +} + +.modal-input::-webkit-calendar-picker-indicator { + filter: invert(1); + cursor: pointer; +} + +/* Badge zakresu wykresu */ +.chart-range-badge { + position: absolute; + top: 10px; + right: 10px; + background: rgba(22, 27, 34, 0.9); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 4px 10px; + font-size: 0.7rem; + color: #8b949e; + z-index: 10; + pointer-events: none; + backdrop-filter: blur(4px); + font-family: monospace; +} + +@media (max-width: 576px) { + .chart-range-badge { + font-size: 0.65rem; + padding: 3px 8px; + top: 8px; + right: 8px; + } +} + diff --git a/static/js/gauge.js b/static/js/gauge.js new file mode 100644 index 0000000..31c3baf --- /dev/null +++ b/static/js/gauge.js @@ -0,0 +1,20 @@ +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'); +} diff --git a/static/js/modal.js b/static/js/modal.js new file mode 100644 index 0000000..65a0f47 --- /dev/null +++ b/static/js/modal.js @@ -0,0 +1,96 @@ +/** + * Otwiera modal z wyborem własnego zakresu dat + */ +function openCustomRangePicker() { + const modal = document.getElementById('customRangeModal'); + if (!modal) return; + + // Ustaw domyślne wartości - od 24h temu do teraz + const now = new Date(); + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); + + document.getElementById('customStartDate').value = formatDateTimeLocal(yesterday); + document.getElementById('customEndDate').value = formatDateTimeLocal(now); + + modal.style.display = 'flex'; +} + +/** + * Zamyka modal z wyborem zakresu + */ +function closeCustomRangePicker() { + const modal = document.getElementById('customRangeModal'); + if (modal) modal.style.display = 'none'; +} + +/** + * Stosuje wybrany własny zakres + */ +async function applyCustomRange() { + const startInput = document.getElementById('customStartDate'); + const endInput = document.getElementById('customEndDate'); + + if (!startInput.value || !endInput.value) { + alert('Wybierz obie daty'); + return; + } + + const startDate = new Date(startInput.value); + const endDate = new Date(endInput.value); + const now = new Date(); + + // Walidacja + if (startDate >= endDate) { + alert('Data początkowa musi być wcześniejsza niż końcowa'); + return; + } + + if (endDate > now) { + alert('Data końcowa nie może być w przyszłości'); + return; + } + + const diffDays = (endDate - startDate) / (1000 * 60 * 60 * 24); + if (diffDays > 30) { + alert('Maksymalny zakres to 30 dni'); + return; + } + + closeCustomRangePicker(); + document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active')); + document.getElementById('customRangeBtn').classList.add('active'); + + currentTimeRange = 'custom'; + + const startTime = startDate.getTime(); + const endTime = endDate.getTime(); + + await reloadDataForRange(startTime, endTime); + + if (voltageChart) { + voltageChart.options.scales.x.min = startTime; + voltageChart.options.scales.x.max = endTime; + voltageChart.update('none'); + } +} + +/** + * Formatuje datę do formatu datetime-local input + */ +function formatDateTimeLocal(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + return `${year}-${month}-${day}T${hours}:${minutes}`; +} + +// Zamknij modal po kliknięciu poza nim +document.addEventListener('click', function(event) { + const modal = document.getElementById('customRangeModal'); + if (modal && event.target === modal) { + closeCustomRangePicker(); + } +}); diff --git a/static/js/monitor.js b/static/js/monitor.js index 6722093..4671dc4 100644 --- a/static/js/monitor.js +++ b/static/js/monitor.js @@ -34,6 +34,7 @@ function initMonitor(phases, defaultRange) { window.changeTimeRange(currentTimeRange); } + /** * Konfiguracja wykresu głównego (Zoom + Pan) */ @@ -54,27 +55,47 @@ function setupMainChart() { }, scales: { x: { - type: 'time', - time: { - displayFormats: { hour: 'HH:mm', minute: 'HH:mm' }, - tooltipFormat: 'yyyy-MM-dd HH:mm:ss' - }, + type: 'time', + time: { + displayFormats: { + millisecond: 'HH:mm:ss.SSS', + second: 'HH:mm:ss', + minute: 'HH:mm', + hour: 'HH:mm', + day: 'dd LLL', + week: 'dd LLL', + month: 'LLL yyyy', + quarter: 'LLL yyyy', + year: 'yyyy' + }, + tooltipFormat: 'dd.MM.yyyy HH:mm:ss' + }, + grid: { color: '#2d3139' }, ticks: { color: '#8b949e' } }, y: { - beginAtZero: false, - suggestedMin: 200, - suggestedMax: 250, + beginAtZero: false, + min: 190, + max: 255, grid: { color: '#2d3139' }, - ticks: { stepSize: 5, color: '#c9d1d9' } + ticks: { + stepSize: 5, + color: '#c9d1d9' + } } }, plugins: { zoom: { limits: { x: { - rangeMax: 30 * 24 * 60 * 60 * 1000 + min: 'original', + max: () => Date.now(), + minRange: 60 * 1000 + }, + y: { + min: 190, + max: 255 } }, zoom: { @@ -86,15 +107,30 @@ function setupMainChart() { }, mode: 'x', onZoomComplete: async ({chart}) => { + const now = Date.now(); + if (chart.scales.x.max > now) { + chart.scales.x.max = now; + chart.update('none'); + } + 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', + mode: 'x', onPanComplete: async ({chart}) => { + const now = Date.now(); + if (chart.scales.x.max > now) { + const rangeWidth = chart.scales.x.max - chart.scales.x.min; + chart.scales.x.max = now; + chart.scales.x.min = now - rangeWidth; + chart.update('none'); + } + document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active')); currentTimeRange = 'precise'; await reloadDataForRange(chart.scales.x.min, chart.scales.x.max); @@ -116,7 +152,7 @@ function setupMainChart() { 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('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`; @@ -126,67 +162,88 @@ function setupMainChart() { } } }); - } /** * 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 now = Date.now(); + if (max && max > now) { + max = now; + } + if (min && min > now) { + min = now - 3600000; + } + 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.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); + } 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); + } catch (e) { + console.error("Błąd pobierania zdarzeń", e); } - + voltageChart.data.datasets = newDatasets; if (rangeName) { - voltageChart.update(); + 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' }; - + + let rangeText = ''; + if (start.toDateString() === end.toDateString()) { - label.textContent = `Zakres: ${start.toLocaleDateString('pl-PL', optDate)}, ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleTimeString('pl-PL', optTime)}`; + rangeText = `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)}`; + rangeText = `Zakres: ${start.toLocaleDateString('pl-PL', optDate)} ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleDateString('pl-PL', optDate)} ${end.toLocaleTimeString('pl-PL', optTime)}`; + } + + // Aktualizuj label nad logami + const label = document.getElementById('eventRangeLabel'); + if (label) { + label.textContent = rangeText; + } + + const chartDisplay = document.getElementById('chartRangeDisplay'); + if (chartDisplay) { + const optDateShort = { day: '2-digit', month: '2-digit' }; + if (start.toDateString() === end.toDateString()) { + chartDisplay.textContent = `${start.toLocaleDateString('pl-PL', optDateShort)} ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleTimeString('pl-PL', optTime)}`; + } else { + chartDisplay.textContent = `${start.toLocaleDateString('pl-PL', optDateShort)} ${start.toLocaleTimeString('pl-PL', optTime)} - ${end.toLocaleDateString('pl-PL', optDateShort)} ${end.toLocaleTimeString('pl-PL', optTime)}`; + } } } @@ -332,26 +389,4 @@ window.showEventOnChart = function(startTimeStr) { currentTimeRange = 'precise'; reloadDataForRange(min, max); } -}; - - -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'); -} +}; \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 47a1287..eb33d94 100644 --- a/templates/base.html +++ b/templates/base.html @@ -16,10 +16,15 @@ + + + + + + + - - - + {% block scripts %}{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 96d2e46..9058c2e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -42,11 +42,38 @@ {{ key }} {% endfor %} + + + + + +
+
-
+
+
@@ -62,8 +89,9 @@ {% endblock %} {% block scripts %} + - +