tx bias chart

This commit is contained in:
Mateusz Gruszczyński
2026-01-03 20:13:05 +01:00
parent bea108c593
commit 840d41bbb8
3 changed files with 503 additions and 357 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -7,13 +7,13 @@ class GPONCharts {
init() { init() {
this.createOpticalChart(); this.createOpticalChart();
this.createTemperatureChart(); this.createTemperatureChart();
this.createTXBiasChart();
this.createTrafficChart(); this.createTrafficChart();
this.createBytesChart(); this.createBytesChart();
this.createVolumeChart(); this.createVolumeChart();
this.createFECChart(); this.createFECChart();
this.setupGlobalPeriodSelector(); // Period selectors
document.getElementById('period-optical').addEventListener('change', (e) => { document.getElementById('period-optical').addEventListener('change', (e) => {
this.updateOpticalChart(e.target.value); this.updateOpticalChart(e.target.value);
}); });
@@ -22,6 +22,10 @@ class GPONCharts {
this.updateTemperatureChart(e.target.value); this.updateTemperatureChart(e.target.value);
}); });
document.getElementById('period-txbias').addEventListener('change', (e) => {
this.updateTXBiasChart(e.target.value);
});
document.getElementById('period-traffic').addEventListener('change', (e) => { document.getElementById('period-traffic').addEventListener('change', (e) => {
this.updateTrafficChart(e.target.value); this.updateTrafficChart(e.target.value);
}); });
@@ -38,22 +42,30 @@ class GPONCharts {
this.updateFECChart(e.target.value); this.updateFECChart(e.target.value);
}); });
// Volume chart type switcher
document.querySelectorAll('input[name="volume-chart-type"]').forEach(radio => { document.querySelectorAll('input[name="volume-chart-type"]').forEach(radio => {
radio.addEventListener('change', (e) => { radio.addEventListener('change', (e) => {
this.changeVolumeChartType(e.target.value); this.changeVolumeChartType(e.target.value);
}); });
}); });
// Global period selector
this.setupGlobalPeriodSelector();
// Initial load
this.updateOpticalChart('1h'); this.updateOpticalChart('1h');
this.updateTemperatureChart('1h'); this.updateTemperatureChart('1h');
this.updateTXBiasChart('7d'); // Default 7d for trends
this.updateTrafficChart('1h'); this.updateTrafficChart('1h');
this.updateBytesChart('1h'); this.updateBytesChart('1h');
this.updateVolumeChart('1h'); this.updateVolumeChart('1h');
this.updateFECChart('1h'); this.updateFECChart('1h');
// Auto-refresh every 30s
setInterval(() => { setInterval(() => {
const optPeriod = document.getElementById('period-optical').value; const optPeriod = document.getElementById('period-optical').value;
const tempPeriod = document.getElementById('period-temperature').value; const tempPeriod = document.getElementById('period-temperature').value;
const txbiasPeriod = document.getElementById('period-txbias').value;
const trafficPeriod = document.getElementById('period-traffic').value; const trafficPeriod = document.getElementById('period-traffic').value;
const bytesPeriod = document.getElementById('period-bytes').value; const bytesPeriod = document.getElementById('period-bytes').value;
const volumePeriod = document.getElementById('period-volume').value; const volumePeriod = document.getElementById('period-volume').value;
@@ -61,6 +73,7 @@ class GPONCharts {
this.updateOpticalChart(optPeriod); this.updateOpticalChart(optPeriod);
this.updateTemperatureChart(tempPeriod); this.updateTemperatureChart(tempPeriod);
this.updateTXBiasChart(txbiasPeriod);
this.updateTrafficChart(trafficPeriod); this.updateTrafficChart(trafficPeriod);
this.updateBytesChart(bytesPeriod); this.updateBytesChart(bytesPeriod);
this.updateVolumeChart(volumePeriod); this.updateVolumeChart(volumePeriod);
@@ -72,13 +85,12 @@ class GPONCharts {
setupGlobalPeriodSelector() { setupGlobalPeriodSelector() {
const radios = document.querySelectorAll('input[name="global-period"]'); const radios = document.querySelectorAll('input[name="global-period"]');
radios.forEach(radio => { radios.forEach(radio => {
radio.addEventListener('change', (e) => { radio.addEventListener('change', (e) => {
const period = e.target.value; const period = e.target.value;
document.getElementById('period-optical').value = period; document.getElementById('period-optical').value = period;
document.getElementById('period-temperature').value = period; document.getElementById('period-temperature').value = period;
document.getElementById('period-txbias').value = period;
document.getElementById('period-traffic').value = period; document.getElementById('period-traffic').value = period;
document.getElementById('period-bytes').value = period; document.getElementById('period-bytes').value = period;
document.getElementById('period-volume').value = period; document.getElementById('period-volume').value = period;
@@ -86,53 +98,59 @@ class GPONCharts {
this.updateOpticalChart(period); this.updateOpticalChart(period);
this.updateTemperatureChart(period); this.updateTemperatureChart(period);
this.updateTXBiasChart(period);
this.updateTrafficChart(period); this.updateTrafficChart(period);
this.updateBytesChart(period); this.updateBytesChart(period);
this.updateVolumeChart(period); this.updateVolumeChart(period);
this.updateFECChart(period); this.updateFECChart(period);
console.log('[Charts] Global period: ' + period); console.log('[Charts] Global period:', period);
}); });
}); });
} }
createOpticalChart() { createOpticalChart() {
const ctx = document.getElementById('chart-optical').getContext('2d'); const ctx = document.getElementById('chart-optical').getContext('2d');
this.charts.optical = new Chart(ctx, { this.charts.optical = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: [], labels: [],
datasets: [ datasets: [{
{ label: 'RX Power (dBm)',
label: 'RX Power (dBm)', data: [],
data: [], borderColor: '#00ccff',
borderColor: '#00ccff', backgroundColor: 'rgba(0, 204, 255, 0.1)',
backgroundColor: 'rgba(0, 204, 255, 0.1)', borderWidth: 2,
borderWidth: 2, tension: 0.4,
tension: 0.4, fill: true,
fill: true, pointRadius: 0
pointRadius: 0 }, {
}, label: 'TX Power (dBm)',
{ data: [],
label: 'TX Power (dBm)', borderColor: '#ffcc00',
data: [], backgroundColor: 'rgba(255, 204, 0, 0.1)',
borderColor: '#ffcc00', borderWidth: 2,
backgroundColor: 'rgba(255, 204, 0, 0.1)', tension: 0.4,
borderWidth: 2, fill: true,
tension: 0.4, pointRadius: 0
fill: true, }]
pointRadius: 0
}
]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false }, interaction: {
mode: 'index',
intersect: false
},
plugins: { plugins: {
legend: { display: true, labels: { color: '#ffffff' } }, legend: {
tooltip: { mode: 'index', intersect: false } display: true,
labels: { color: '#ffffff' }
},
tooltip: {
mode: 'index',
intersect: false
}
}, },
scales: { scales: {
x: { x: {
@@ -142,7 +160,11 @@ class GPONCharts {
y: { y: {
ticks: { color: '#b0b0b0' }, ticks: { color: '#b0b0b0' },
grid: { color: 'rgba(255, 255, 255, 0.1)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' },
title: { display: true, text: 'dBm', color: '#b0b0b0' } title: {
display: true,
text: 'dBm',
color: '#b0b0b0'
}
} }
} }
} }
@@ -151,7 +173,6 @@ class GPONCharts {
createTemperatureChart() { createTemperatureChart() {
const ctx = document.getElementById('chart-temperature').getContext('2d'); const ctx = document.getElementById('chart-temperature').getContext('2d');
this.charts.temperature = new Chart(ctx, { this.charts.temperature = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
@@ -171,11 +192,14 @@ class GPONCharts {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { display: true, labels: { color: '#ffffff' } }, legend: {
display: true,
labels: { color: '#ffffff' }
},
tooltip: { tooltip: {
callbacks: { callbacks: {
label: function(context) { label: function(context) {
return context.parsed.y ? context.parsed.y.toFixed(1) + '°C' : 'N/A'; return context.parsed.y ? `${context.parsed.y.toFixed(1)}°C` : 'N/A';
} }
} }
} }
@@ -188,7 +212,68 @@ class GPONCharts {
y: { y: {
ticks: { color: '#b0b0b0' }, ticks: { color: '#b0b0b0' },
grid: { color: 'rgba(255, 255, 255, 0.1)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' },
title: { display: true, text: '°C', color: '#b0b0b0' } title: {
display: true,
text: '°C',
color: '#b0b0b0'
}
}
}
}
});
}
createTXBiasChart() {
const ctx = document.getElementById('chart-txbias').getContext('2d');
this.charts.txbias = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'TX Bias (mA)',
data: [],
borderColor: '#9b59b6',
backgroundColor: 'rgba(155, 89, 182, 0.1)',
borderWidth: 2,
tension: 0.4,
fill: true,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: { color: '#ffffff' }
},
tooltip: {
callbacks: {
label: function(context) {
return context.parsed.y ? `${context.parsed.y.toFixed(2)} mA` : 'N/A';
}
}
}
},
scales: {
x: {
ticks: { color: '#b0b0b0', maxRotation: 0 },
grid: { color: 'rgba(255, 255, 255, 0.1)' }
},
y: {
ticks: {
color: '#b0b0b0',
callback: function(value) {
return value.toFixed(1) + ' mA';
}
},
grid: { color: 'rgba(255, 255, 255, 0.1)' },
title: {
display: true,
text: 'mA',
color: '#b0b0b0'
}
} }
} }
} }
@@ -197,40 +282,42 @@ class GPONCharts {
createTrafficChart() { createTrafficChart() {
const ctx = document.getElementById('chart-traffic').getContext('2d'); const ctx = document.getElementById('chart-traffic').getContext('2d');
this.charts.traffic = new Chart(ctx, { this.charts.traffic = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: [], labels: [],
datasets: [ datasets: [{
{ label: 'RX (Packets/s)',
label: 'RX Packets/s', data: [],
data: [], borderColor: '#4bc0c0',
borderColor: '#4bc0c0', backgroundColor: 'rgba(75, 192, 192, 0.1)',
backgroundColor: 'rgba(75, 192, 192, 0.1)', borderWidth: 2,
borderWidth: 2, tension: 0.4,
tension: 0.4, fill: true,
fill: true, pointRadius: 0
pointRadius: 0 }, {
}, label: 'TX (Packets/s)',
{ data: [],
label: 'TX Packets/s', borderColor: '#ff6384',
data: [], backgroundColor: 'rgba(255, 99, 132, 0.1)',
borderColor: '#ff6384', borderWidth: 2,
backgroundColor: 'rgba(255, 99, 132, 0.1)', tension: 0.4,
borderWidth: 2, fill: true,
tension: 0.4, pointRadius: 0
fill: true, }]
pointRadius: 0
}
]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { display: true, labels: { color: '#ffffff' } }, legend: {
tooltip: { mode: 'index', intersect: false } display: true,
labels: { color: '#ffffff' }
},
tooltip: {
mode: 'index',
intersect: false
}
}, },
scales: { scales: {
x: { x: {
@@ -240,7 +327,11 @@ class GPONCharts {
y: { y: {
ticks: { color: '#b0b0b0' }, ticks: { color: '#b0b0b0' },
grid: { color: 'rgba(255, 255, 255, 0.1)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' },
title: { display: true, text: 'packets/s', color: '#b0b0b0' } title: {
display: true,
text: 'packets/s',
color: '#b0b0b0'
}
} }
} }
} }
@@ -249,46 +340,44 @@ class GPONCharts {
createBytesChart() { createBytesChart() {
const ctx = document.getElementById('chart-bytes').getContext('2d'); const ctx = document.getElementById('chart-bytes').getContext('2d');
this.charts.bytes = new Chart(ctx, { this.charts.bytes = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: [], labels: [],
datasets: [ datasets: [{
{ label: 'RX (Mb/s)',
label: 'RX (Mb/s)', data: [],
data: [], borderColor: '#4bc0c0',
borderColor: '#4bc0c0', backgroundColor: 'rgba(75, 192, 192, 0.1)',
backgroundColor: 'rgba(75, 192, 192, 0.1)', borderWidth: 2,
borderWidth: 2, tension: 0.4,
tension: 0.4, fill: true,
fill: true, pointRadius: 0
pointRadius: 0 }, {
}, label: 'TX (Mb/s)',
{ data: [],
label: 'TX (Mb/s)', borderColor: '#ff6384',
data: [], backgroundColor: 'rgba(255, 99, 132, 0.1)',
borderColor: '#ff6384', borderWidth: 2,
backgroundColor: 'rgba(255, 99, 132, 0.1)', tension: 0.4,
borderWidth: 2, fill: true,
tension: 0.4, pointRadius: 0
fill: true, }]
pointRadius: 0
}
]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { display: true, labels: { color: '#ffffff' } }, legend: {
display: true,
labels: { color: '#ffffff' }
},
tooltip: { tooltip: {
mode: 'index', mode: 'index',
intersect: false, intersect: false,
callbacks: { callbacks: {
label: function(context) { label: function(context) {
return context.dataset.label + ': ' + return `${context.dataset.label}: ${context.parsed.y ? context.parsed.y.toFixed(2) : 0} Mb/s`;
(context.parsed.y ? context.parsed.y.toFixed(2) : '0') + ' Mb/s';
} }
} }
} }
@@ -306,7 +395,11 @@ class GPONCharts {
} }
}, },
grid: { color: 'rgba(255, 255, 255, 0.1)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' },
title: { display: true, text: 'Mb/s', color: '#b0b0b0' } title: {
display: true,
text: 'Mb/s',
color: '#b0b0b0'
}
} }
} }
} }
@@ -321,34 +414,34 @@ class GPONCharts {
type: 'line', type: 'line',
data: { data: {
labels: [], labels: [],
datasets: [ datasets: [{
{ label: 'RX (per minute)',
label: 'RX per minute', data: [],
data: [], borderColor: '#4bc0c0',
borderColor: '#4bc0c0', backgroundColor: 'rgba(75, 192, 192, 0.05)',
backgroundColor: 'rgba(75, 192, 192, 0.05)', borderWidth: 2,
borderWidth: 2, tension: 0.4,
tension: 0.4, fill: true,
fill: true, pointRadius: 0
pointRadius: 0 }, {
}, label: 'TX (per minute)',
{ data: [],
label: 'TX per minute', borderColor: '#ff6384',
data: [], backgroundColor: 'rgba(255, 99, 132, 0.05)',
borderColor: '#ff6384', borderWidth: 2,
backgroundColor: 'rgba(255, 99, 132, 0.05)', tension: 0.4,
borderWidth: 2, fill: true,
tension: 0.4, pointRadius: 0
fill: true, }]
pointRadius: 0
}
]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { display: true, labels: { color: '#ffffff' } }, legend: {
display: true,
labels: { color: '#ffffff' }
},
tooltip: { tooltip: {
mode: 'index', mode: 'index',
intersect: false, intersect: false,
@@ -356,7 +449,7 @@ class GPONCharts {
label: function(context) { label: function(context) {
const bytes = context.parsed.y; const bytes = context.parsed.y;
const formatted = self.formatBytes(bytes); const formatted = self.formatBytes(bytes);
return context.dataset.label + ': ' + formatted; return `${context.dataset.label}: ${formatted}`;
} }
} }
} }
@@ -376,7 +469,11 @@ class GPONCharts {
} }
}, },
grid: { color: 'rgba(255, 255, 255, 0.1)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' },
title: { display: true, text: 'Data per minute', color: '#b0b0b0' } title: {
display: true,
text: 'Data per minute',
color: '#b0b0b0'
}
} }
} }
} }
@@ -385,39 +482,38 @@ class GPONCharts {
createFECChart() { createFECChart() {
const ctx = document.getElementById('chart-fec').getContext('2d'); const ctx = document.getElementById('chart-fec').getContext('2d');
this.charts.fec = new Chart(ctx, { this.charts.fec = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: [], labels: [],
datasets: [ datasets: [{
{ label: 'FEC Corrected',
label: 'FEC Corrected', data: [],
data: [], borderColor: '#ffa500',
borderColor: '#ffa500', backgroundColor: 'rgba(255, 165, 0, 0.1)',
backgroundColor: 'rgba(255, 165, 0, 0.1)', borderWidth: 2,
borderWidth: 2, tension: 0.4,
tension: 0.4, fill: true,
fill: true, pointRadius: 0
pointRadius: 0 }, {
}, label: 'FEC Uncorrected',
{ data: [],
label: 'FEC Uncorrected', borderColor: '#ff0000',
data: [], backgroundColor: 'rgba(255, 0, 0, 0.1)',
borderColor: '#ff0000', borderWidth: 2,
backgroundColor: 'rgba(255, 0, 0, 0.1)', tension: 0.4,
borderWidth: 2, fill: true,
tension: 0.4, pointRadius: 0
fill: true, }]
pointRadius: 0
}
]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { display: true, labels: { color: '#ffffff' } } legend: {
display: true,
labels: { color: '#ffffff' }
}
}, },
scales: { scales: {
x: { x: {
@@ -427,7 +523,11 @@ class GPONCharts {
y: { y: {
ticks: { color: '#b0b0b0' }, ticks: { color: '#b0b0b0' },
grid: { color: 'rgba(255, 255, 255, 0.1)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' },
title: { display: true, text: 'errors/s', color: '#b0b0b0' } title: {
display: true,
text: 'errors/s',
color: '#b0b0b0'
}
} }
} }
} }
@@ -445,6 +545,7 @@ class GPONCharts {
}; };
this.charts.volume.destroy(); this.charts.volume.destroy();
const ctx = document.getElementById('chart-volume').getContext('2d'); const ctx = document.getElementById('chart-volume').getContext('2d');
const self = this; const self = this;
@@ -452,34 +553,34 @@ class GPONCharts {
type: type, type: type,
data: { data: {
labels: currentData.labels, labels: currentData.labels,
datasets: [ datasets: [{
{ label: 'RX (per minute)',
label: 'RX per minute', data: currentData.datasets[0].data,
data: currentData.datasets[0].data, borderColor: '#4bc0c0',
borderColor: '#4bc0c0', backgroundColor: type === 'bar' ? 'rgba(75, 192, 192, 0.6)' : 'rgba(75, 192, 192, 0.05)',
backgroundColor: type === 'bar' ? 'rgba(75, 192, 192, 0.6)' : 'rgba(75, 192, 192, 0.05)', borderWidth: type === 'bar' ? 1 : 2,
borderWidth: type === 'bar' ? 1 : 2, tension: 0.4,
tension: 0.4, fill: true,
fill: true, pointRadius: 0
pointRadius: 0 }, {
}, label: 'TX (per minute)',
{ data: currentData.datasets[1].data,
label: 'TX per minute', borderColor: '#ff6384',
data: currentData.datasets[1].data, backgroundColor: type === 'bar' ? 'rgba(255, 99, 132, 0.6)' : 'rgba(255, 99, 132, 0.05)',
borderColor: '#ff6384', borderWidth: type === 'bar' ? 1 : 2,
backgroundColor: type === 'bar' ? 'rgba(255, 99, 132, 0.6)' : 'rgba(255, 99, 132, 0.05)', tension: 0.4,
borderWidth: type === 'bar' ? 1 : 2, fill: true,
tension: 0.4, pointRadius: 0
fill: true, }]
pointRadius: 0
}
]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { display: true, labels: { color: '#ffffff' } }, legend: {
display: true,
labels: { color: '#ffffff' }
},
tooltip: { tooltip: {
mode: 'index', mode: 'index',
intersect: false, intersect: false,
@@ -487,7 +588,7 @@ class GPONCharts {
label: function(context) { label: function(context) {
const bytes = context.parsed.y; const bytes = context.parsed.y;
const formatted = self.formatBytes(bytes); const formatted = self.formatBytes(bytes);
return context.dataset.label + ': ' + formatted; return `${context.dataset.label}: ${formatted}`;
} }
} }
} }
@@ -507,7 +608,11 @@ class GPONCharts {
} }
}, },
grid: { color: 'rgba(255, 255, 255, 0.1)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' },
title: { display: true, text: 'Data per minute', color: '#b0b0b0' } title: {
display: true,
text: 'Data per minute',
color: '#b0b0b0'
}
} }
} }
} }
@@ -518,7 +623,7 @@ class GPONCharts {
async updateOpticalChart(period) { async updateOpticalChart(period) {
try { try {
const response = await fetch('/api/history/optical/' + period); const response = await fetch(`/api/history/optical/${period}`);
const data = await response.json(); const data = await response.json();
if (!data || !data.timestamps || data.timestamps.length === 0) { if (!data || !data.timestamps || data.timestamps.length === 0) {
@@ -527,12 +632,10 @@ class GPONCharts {
} }
const labels = this.formatLabels(data.timestamps, period); const labels = this.formatLabels(data.timestamps, period);
this.charts.optical.data.labels = labels; this.charts.optical.data.labels = labels;
this.charts.optical.data.datasets[0].data = data.data.rx_power || []; this.charts.optical.data.datasets[0].data = data.data.rx_power;
this.charts.optical.data.datasets[1].data = data.data.tx_power || []; this.charts.optical.data.datasets[1].data = data.data.tx_power;
this.charts.optical.update(); this.charts.optical.update();
} catch (error) { } catch (error) {
console.error('[Charts] Error optical:', error); console.error('[Charts] Error optical:', error);
} }
@@ -540,39 +643,57 @@ class GPONCharts {
async updateTemperatureChart(period) { async updateTemperatureChart(period) {
try { try {
const response = await fetch('/api/history/optical/' + period); const response = await fetch(`/api/history/optical/${period}`);
const data = await response.json(); const data = await response.json();
if (!data || !data.timestamps || data.timestamps.length === 0) return; if (!data || !data.timestamps || data.timestamps.length === 0) {
return;
}
const labels = this.formatLabels(data.timestamps, period); const labels = this.formatLabels(data.timestamps, period);
this.charts.temperature.data.labels = labels; this.charts.temperature.data.labels = labels;
this.charts.temperature.data.datasets[0].data = data.data.temperature || []; this.charts.temperature.data.datasets[0].data = data.data.temperature;
this.charts.temperature.update(); this.charts.temperature.update();
} catch (error) { } catch (error) {
console.error('[Charts] Error temperature:', error); console.error('[Charts] Error temperature:', error);
} }
} }
async updateTrafficChart(period) { async updateTXBiasChart(period) {
try { try {
const response = await fetch('/api/history/traffic/' + period); const response = await fetch(`/api/history/optical/${period}`);
const data = await response.json(); const data = await response.json();
if (!data || !data.timestamps || data.timestamps.length === 0) return; if (!data || !data.timestamps || data.timestamps.length === 0) {
return;
}
const labels = this.formatLabels(data.timestamps, period); const labels = this.formatLabels(data.timestamps, period);
this.charts.txbias.data.labels = labels;
this.charts.txbias.data.datasets[0].data = data.data.tx_bias;
this.charts.txbias.update();
} catch (error) {
console.error('[Charts] Error TX Bias:', error);
}
}
const rxPps = data.data.rx_packets || []; async updateTrafficChart(period) {
const txPps = data.data.tx_packets || []; try {
const response = await fetch(`/api/history/traffic/${period}`);
const data = await response.json();
if (!data || !data.timestamps || data.timestamps.length === 0) {
return;
}
const labels = this.formatLabels(data.timestamps, period);
const rxPps = data.data.rx_packets;
const txPps = data.data.tx_packets;
this.charts.traffic.data.labels = labels; this.charts.traffic.data.labels = labels;
this.charts.traffic.data.datasets[0].data = rxPps; this.charts.traffic.data.datasets[0].data = rxPps;
this.charts.traffic.data.datasets[1].data = txPps; this.charts.traffic.data.datasets[1].data = txPps;
this.charts.traffic.update(); this.charts.traffic.update();
} catch (error) { } catch (error) {
console.error('[Charts] Error traffic:', error); console.error('[Charts] Error traffic:', error);
} }
@@ -580,19 +701,16 @@ class GPONCharts {
async updateBytesChart(period) { async updateBytesChart(period) {
try { try {
const response = await fetch('/api/history/traffic/' + period); const response = await fetch(`/api/history/traffic/${period}`);
const data = await response.json(); const data = await response.json();
if (!data || !data.timestamps || data.timestamps.length === 0) return; if (!data || !data.timestamps || data.timestamps.length === 0) {
return;
}
const labels = this.formatLabels(data.timestamps, period); const labels = this.formatLabels(data.timestamps, period);
const rxMbps = data.data.rx_bytes.map(v => v !== null ? (v * 8) / 1000000 : null);
const rxMbps = (data.data.rx_bytes || []).map(v => const txMbps = data.data.tx_bytes.map(v => v !== null ? (v * 8) / 1000000 : null);
v !== null ? (v * 8) / 1000000 : null
);
const txMbps = (data.data.tx_bytes || []).map(v =>
v !== null ? (v * 8) / 1000000 : null
);
this.charts.bytes.data.labels = labels; this.charts.bytes.data.labels = labels;
this.charts.bytes.data.datasets[0].data = rxMbps; this.charts.bytes.data.datasets[0].data = rxMbps;
@@ -600,7 +718,6 @@ class GPONCharts {
this.charts.bytes.update(); this.charts.bytes.update();
console.log('[Charts] Bytes - max RX Mb/s:', Math.max(...rxMbps.filter(v => v !== null)).toFixed(2)); console.log('[Charts] Bytes - max RX Mb/s:', Math.max(...rxMbps.filter(v => v !== null)).toFixed(2));
} catch (error) { } catch (error) {
console.error('[Charts] Error bytes:', error); console.error('[Charts] Error bytes:', error);
} }
@@ -608,23 +725,20 @@ class GPONCharts {
async updateVolumeChart(period) { async updateVolumeChart(period) {
try { try {
const response = await fetch('/api/history/traffic/' + period); const response = await fetch(`/api/history/traffic/${period}`);
const data = await response.json(); const data = await response.json();
if (!data || !data.timestamps || data.timestamps.length === 0) return; if (!data || !data.timestamps || data.timestamps.length === 0) {
return;
}
const labels = this.formatLabels(data.timestamps, period); const labels = this.formatLabels(data.timestamps, period);
const rxBytesRate = data.data.rx_bytes;
const rxBytesRate = data.data.rx_bytes || []; const txBytesRate = data.data.tx_bytes;
const txBytesRate = data.data.tx_bytes || [];
const step = data.step || 60; const step = data.step || 60;
const rxPerInterval = rxBytesRate.map(rate => const rxPerInterval = rxBytesRate.map(rate => rate !== null ? rate * step : null);
rate !== null ? rate * step : null const txPerInterval = txBytesRate.map(rate => rate !== null ? rate * step : null);
);
const txPerInterval = txBytesRate.map(rate =>
rate !== null ? rate * step : null
);
this.charts.volume.data.labels = labels; this.charts.volume.data.labels = labels;
this.charts.volume.data.datasets[0].data = rxPerInterval; this.charts.volume.data.datasets[0].data = rxPerInterval;
@@ -633,7 +747,6 @@ class GPONCharts {
const maxRx = Math.max(...rxPerInterval.filter(v => v !== null)); const maxRx = Math.max(...rxPerInterval.filter(v => v !== null));
console.log('[Charts] Volume - max per minute:', this.formatBytes(maxRx)); console.log('[Charts] Volume - max per minute:', this.formatBytes(maxRx));
} catch (error) { } catch (error) {
console.error('[Charts] Error volume:', error); console.error('[Charts] Error volume:', error);
} }
@@ -641,21 +754,21 @@ class GPONCharts {
async updateFECChart(period) { async updateFECChart(period) {
try { try {
const response = await fetch('/api/history/fec/' + period); const response = await fetch(`/api/history/fec/${period}`);
const data = await response.json(); const data = await response.json();
if (!data || !data.timestamps || data.timestamps.length === 0) return; if (!data || !data.timestamps || data.timestamps.length === 0) {
return;
}
const labels = this.formatLabels(data.timestamps, period); const labels = this.formatLabels(data.timestamps, period);
const correctedRate = data.data.corrected;
const correctedRate = data.data.corrected || []; const uncorrectedRate = data.data.uncorrected;
const uncorrectedRate = data.data.uncorrected || [];
this.charts.fec.data.labels = labels; this.charts.fec.data.labels = labels;
this.charts.fec.data.datasets[0].data = correctedRate; this.charts.fec.data.datasets[0].data = correctedRate;
this.charts.fec.data.datasets[1].data = uncorrectedRate; this.charts.fec.data.datasets[1].data = uncorrectedRate;
this.charts.fec.update(); this.charts.fec.update();
} catch (error) { } catch (error) {
console.error('[Charts] Error FEC:', error); console.error('[Charts] Error FEC:', error);
} }
@@ -664,12 +777,10 @@ class GPONCharts {
formatLabels(timestamps, period) { formatLabels(timestamps, period) {
return timestamps.map(ts => { return timestamps.map(ts => {
const date = new Date(ts * 1000); const date = new Date(ts * 1000);
if (period === '7d' || period === '14d' || period === '30d' || period === '60d' || period === '90d') { if (period === '7d' || period === '14d' || period === '30d' || period === '60d' || period === '90d') {
return date.toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit' }) + ' ' + return date.toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit' }) + ' ' +
date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }); date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' });
} }
return date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }); return date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' });
}); });
} }
@@ -677,12 +788,10 @@ class GPONCharts {
formatBytes(bytes, decimals = 2) { formatBytes(bytes, decimals = 2) {
if (bytes === null || bytes === undefined) return 'N/A'; if (bytes === null || bytes === undefined) return 'N/A';
if (bytes === 0) return '0 B'; if (bytes === 0) return '0 B';
const k = 1024; const k = 1024;
const dm = decimals < 0 ? 0 : decimals; const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
} }
} }

View File

@@ -4,69 +4,75 @@
<!-- Alerts Container --> <!-- Alerts Container -->
<div id="alerts-container" class="mb-4"></div> <div id="alerts-container" class="mb-4"></div>
<!-- Global Period Selector --> <!-- Global Period Selector -->
<div class="card-body py-3"> <div class="row mb-3">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2"> <div class="col-md-12">
<div class="flex-shrink-0 text-white-50"> <div class="card">
<i class="bi bi-calendar-range me-2"></i> <div class="card-body py-3">
<strong class="text-white">Global Time Range</strong> <div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
</div> <div class="flex-shrink-0 text-white-50">
<i class="bi bi-calendar-range me-2"></i>
<strong class="text-white">Global Time Range</strong>
</div>
<div class="d-flex gap-1 flex-shrink-0 p-1 bg-dark bg-opacity-50 border border-secondary border-opacity-25 rounded-1" <div class="d-flex gap-1 flex-shrink-0 p-1 bg-dark bg-opacity-50 border border-secondary border-opacity-25 rounded-1"
style="max-width: 70%;"> style="max-width: 70%;">
<div class="btn-toolbar d-none d-sm-flex" role="toolbar" id="global-quick-pickers" <div class="btn-toolbar d-none d-sm-flex" role="toolbar" id="global-quick-pickers"
style="overflow-x: auto; scrollbar-width: thin; max-width: 100%;"> style="overflow-x: auto; scrollbar-width: thin; max-width: 100%;">
<div class="btn-group btn-group-sm me-1" role="group"> <div class="btn-group btn-group-sm me-1" role="group">
<input type="radio" class="btn-check" name="global-period" id="period-1h" value="1h" checked> <input type="radio" class="btn-check" name="global-period" id="period-1h" value="1h" checked>
<label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-1h">1h</label> <label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-1h">1h</label>
<input type="radio" class="btn-check" name="global-period" id="period-6h" value="6h"> <input type="radio" class="btn-check" name="global-period" id="period-6h" value="6h">
<label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-6h">6h</label> <label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-6h">6h</label>
<input type="radio" class="btn-check" name="global-period" id="period-12h" value="12h"> <input type="radio" class="btn-check" name="global-period" id="period-12h" value="12h">
<label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-12h">12h</label> <label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-12h">12h</label>
<input type="radio" class="btn-check" name="global-period" id="period-24h" value="24h"> <input type="radio" class="btn-check" name="global-period" id="period-24h" value="24h">
<label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-24h">24h</label> <label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-24h">24h</label>
</div> </div>
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="global-period" id="period-3d" value="3d"> <input type="radio" class="btn-check" name="global-period" id="period-3d" value="3d">
<label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-3d">3d</label> <label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-3d">3d</label>
<input type="radio" class="btn-check" name="global-period" id="period-7d" value="7d"> <input type="radio" class="btn-check" name="global-period" id="period-7d" value="7d">
<label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-7d">7d</label> <label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-7d">7d</label>
<input type="radio" class="btn-check" name="global-period" id="period-30d" value="30d"> <input type="radio" class="btn-check" name="global-period" id="period-30d" value="30d">
<label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-30d">30d</label> <label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-30d">30d</label>
<input type="radio" class="btn-check" name="global-period" id="period-90d" value="90d"> <input type="radio" class="btn-check" name="global-period" id="period-90d" value="90d">
<label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-90d">90d</label> <label class="btn btn-outline-light btn-sm px-2 py-0.5 text-nowrap" for="period-90d">90d</label>
</div>
</div>
<select class="form-select form-select-sm d-md-none ms-2 bg-dark text-white border-secondary"
id="global-period-select" style="min-width: 100px;">
<option value="1h" class="bg-dark text-white">1h</option>
<option value="6h" class="bg-dark text-white">6h</option>
<option value="12h" class="bg-dark text-white">12h</option>
<option value="24h" class="bg-dark text-white">24h</option>
<option value="3d" class="bg-dark text-white">3d</option>
<option value="7d" class="bg-dark text-white">7d</option>
<option value="14d" class="bg-dark text-white">14d</option>
<option value="30d" class="bg-dark text-white">30d</option>
<option value="60d" class="bg-dark text-white">60d</option>
<option value="90d" class="bg-dark text-white">90d</option>
<option value="120d" class="bg-dark text-white">120d</option>
<option value="1y" class="bg-dark text-white">1Y</option>
<option value="2y" class="bg-dark text-white">2Y</option>
<option value="5y" class="bg-dark text-white">5Y</option>
</select>
</div>
<input type="hidden" id="global-period-hidden" name="global-period" value="1h">
</div> </div>
</div> </div>
<select class="form-select form-select-sm d-md-none ms-2 bg-dark text-white border-secondary"
id="global-period-select" style="min-width: 100px;">
<option value="1h" class="bg-dark text-white">1h</option>
<option value="6h" class="bg-dark text-white">6h</option>
<option value="12h" class="bg-dark text-white">12h</option>
<option value="24h" class="bg-dark text-white">24h</option>
<option value="3d" class="bg-dark text-white">3d</option>
<option value="7d" class="bg-dark text-white">7d</option>
<option value="14d" class="bg-dark text-white">14d</option>
<option value="30d" class="bg-dark text-white">30d</option>
<option value="60d" class="bg-dark text-white">60d</option>
<option value="90d" class="bg-dark text-white">90d</option>
<option value="120d" class="bg-dark text-white">120d</option>
<option value="1y" class="bg-dark text-white">1Y</option>
<option value="2y" class="bg-dark text-white">2Y</option>
<option value="5y" class="bg-dark text-white">5Y</option>
</select>
</div> </div>
<input type="hidden" id="global-period-hidden" name="global-period" value="1h">
</div> </div>
</div> </div>
@@ -173,7 +179,7 @@
</div> </div>
</div> </div>
<!-- Charts Row --> <!-- Optical Metrics Row (RX/TX Power + Temperature + TX Bias) -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<!-- Optical Power --> <!-- Optical Power -->
<div class="col-md-4"> <div class="col-md-4">
@@ -231,11 +237,42 @@
</div> </div>
</div> </div>
<!-- Traffic --> <!-- TX Bias -->
<div class="col-md-4"> <div class="col-md-4">
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-activity"></i> Traffic</span> <span><i class="bi bi-lightning-charge"></i> TX Bias Current</span>
<select class="form-select form-select-sm w-auto" id="period-txbias">
<option value="1h">1h</option>
<option value="6h">6h</option>
<option value="12h">12h</option>
<option value="24h">24h</option>
<option value="3d">3d</option>
<option value="7d" selected>7d</option>
<option value="14d">14d</option>
<option value="30d">30d</option>
<option value="60d">60d</option>
<option value="90d">90d</option>
<option value="120d">120d</option>
<option value="1y">1 Year</option>
<option value="2y">2 Years</option>
<option value="5y">5 Years</option>
</select>
</div>
<div class="card-body">
<canvas id="chart-txbias" height="250"></canvas>
</div>
</div>
</div>
</div>
<!-- Traffic Row -->
<div class="row g-3 mb-4">
<!-- Traffic Packets -->
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-activity"></i> Traffic (Packets/s)</span>
<select class="form-select form-select-sm w-auto" id="period-traffic"> <select class="form-select form-select-sm w-auto" id="period-traffic">
<option value="1h" selected>1h</option> <option value="1h" selected>1h</option>
<option value="6h">6h</option> <option value="6h">6h</option>
@@ -254,7 +291,7 @@
</select> </select>
</div> </div>
<div class="card-body"> <div class="card-body">
<canvas id="chart-traffic" height="250"></canvas> <canvas id="chart-traffic" height="200"></canvas>
</div> </div>
</div> </div>
</div> </div>
@@ -326,7 +363,7 @@
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-database"></i> Data Volume (Cumulative)</span> <span><i class="bi bi-database"></i> Data Volume (Cumulative)</span>
<div class="d-flex gap-2 align-items-center"> <div class="d-flex gap-2 align-items-center">
<!-- Przełącznik Line/Bar -->
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="volume-chart-type" id="volume-line" value="line" checked> <input type="radio" class="btn-check" name="volume-chart-type" id="volume-line" value="line" checked>
<label class="btn btn-outline-secondary" for="volume-line"> <label class="btn btn-outline-secondary" for="volume-line">