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 %}