release
This commit is contained in:
692
static/js/charts.js
Normal file
692
static/js/charts.js
Normal file
@@ -0,0 +1,692 @@
|
||||
class GPONCharts {
|
||||
constructor() {
|
||||
this.charts = {};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createOpticalChart();
|
||||
this.createTemperatureChart();
|
||||
this.createTrafficChart();
|
||||
this.createBytesChart();
|
||||
this.createVolumeChart();
|
||||
this.createFECChart();
|
||||
|
||||
this.setupGlobalPeriodSelector();
|
||||
|
||||
document.getElementById('period-optical').addEventListener('change', (e) => {
|
||||
this.updateOpticalChart(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('period-temperature').addEventListener('change', (e) => {
|
||||
this.updateTemperatureChart(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('period-traffic').addEventListener('change', (e) => {
|
||||
this.updateTrafficChart(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('period-bytes').addEventListener('change', (e) => {
|
||||
this.updateBytesChart(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('period-volume').addEventListener('change', (e) => {
|
||||
this.updateVolumeChart(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('period-fec').addEventListener('change', (e) => {
|
||||
this.updateFECChart(e.target.value);
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="volume-chart-type"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
this.changeVolumeChartType(e.target.value);
|
||||
});
|
||||
});
|
||||
|
||||
this.updateOpticalChart('1h');
|
||||
this.updateTemperatureChart('1h');
|
||||
this.updateTrafficChart('1h');
|
||||
this.updateBytesChart('1h');
|
||||
this.updateVolumeChart('1h');
|
||||
this.updateFECChart('1h');
|
||||
|
||||
setInterval(() => {
|
||||
const optPeriod = document.getElementById('period-optical').value;
|
||||
const tempPeriod = document.getElementById('period-temperature').value;
|
||||
const trafficPeriod = document.getElementById('period-traffic').value;
|
||||
const bytesPeriod = document.getElementById('period-bytes').value;
|
||||
const volumePeriod = document.getElementById('period-volume').value;
|
||||
const fecPeriod = document.getElementById('period-fec').value;
|
||||
|
||||
this.updateOpticalChart(optPeriod);
|
||||
this.updateTemperatureChart(tempPeriod);
|
||||
this.updateTrafficChart(trafficPeriod);
|
||||
this.updateBytesChart(bytesPeriod);
|
||||
this.updateVolumeChart(volumePeriod);
|
||||
this.updateFECChart(fecPeriod);
|
||||
}, 30000);
|
||||
|
||||
console.log('[Charts] Initialized');
|
||||
}
|
||||
|
||||
setupGlobalPeriodSelector() {
|
||||
const radios = document.querySelectorAll('input[name="global-period"]');
|
||||
|
||||
radios.forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
const period = e.target.value;
|
||||
|
||||
document.getElementById('period-optical').value = period;
|
||||
document.getElementById('period-temperature').value = period;
|
||||
document.getElementById('period-traffic').value = period;
|
||||
document.getElementById('period-bytes').value = period;
|
||||
document.getElementById('period-volume').value = period;
|
||||
document.getElementById('period-fec').value = period;
|
||||
|
||||
this.updateOpticalChart(period);
|
||||
this.updateTemperatureChart(period);
|
||||
this.updateTrafficChart(period);
|
||||
this.updateBytesChart(period);
|
||||
this.updateVolumeChart(period);
|
||||
this.updateFECChart(period);
|
||||
|
||||
console.log('[Charts] Global period: ' + period);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createOpticalChart() {
|
||||
const ctx = document.getElementById('chart-optical').getContext('2d');
|
||||
|
||||
this.charts.optical = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'RX Power (dBm)',
|
||||
data: [],
|
||||
borderColor: '#00ccff',
|
||||
backgroundColor: 'rgba(0, 204, 255, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'TX Power (dBm)',
|
||||
data: [],
|
||||
borderColor: '#ffcc00',
|
||||
backgroundColor: 'rgba(255, 204, 0, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
plugins: {
|
||||
legend: { display: true, labels: { color: '#ffffff' } },
|
||||
tooltip: { mode: 'index', intersect: false }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#b0b0b0', maxRotation: 0 },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' }
|
||||
},
|
||||
y: {
|
||||
ticks: { color: '#b0b0b0' },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||
title: { display: true, text: 'dBm', color: '#b0b0b0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createTemperatureChart() {
|
||||
const ctx = document.getElementById('chart-temperature').getContext('2d');
|
||||
|
||||
this.charts.temperature = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Temperature (°C)',
|
||||
data: [],
|
||||
borderColor: '#ff6b6b',
|
||||
backgroundColor: 'rgba(255, 107, 107, 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(1) + '°C' : 'N/A';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#b0b0b0', maxRotation: 0 },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' }
|
||||
},
|
||||
y: {
|
||||
ticks: { color: '#b0b0b0' },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||
title: { display: true, text: '°C', color: '#b0b0b0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createTrafficChart() {
|
||||
const ctx = document.getElementById('chart-traffic').getContext('2d');
|
||||
|
||||
this.charts.traffic = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'RX Packets/s',
|
||||
data: [],
|
||||
borderColor: '#4bc0c0',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'TX Packets/s',
|
||||
data: [],
|
||||
borderColor: '#ff6384',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: true, labels: { color: '#ffffff' } },
|
||||
tooltip: { mode: 'index', intersect: false }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#b0b0b0', maxRotation: 0 },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' }
|
||||
},
|
||||
y: {
|
||||
ticks: { color: '#b0b0b0' },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||
title: { display: true, text: 'packets/s', color: '#b0b0b0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createBytesChart() {
|
||||
const ctx = document.getElementById('chart-bytes').getContext('2d');
|
||||
|
||||
this.charts.bytes = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'RX (Mb/s)',
|
||||
data: [],
|
||||
borderColor: '#4bc0c0',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'TX (Mb/s)',
|
||||
data: [],
|
||||
borderColor: '#ff6384',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: true, labels: { color: '#ffffff' } },
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.dataset.label + ': ' +
|
||||
(context.parsed.y ? context.parsed.y.toFixed(2) : '0') + ' Mb/s';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||
title: { display: true, text: 'Mb/s', color: '#b0b0b0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createVolumeChart() {
|
||||
const ctx = document.getElementById('chart-volume').getContext('2d');
|
||||
const self = this;
|
||||
|
||||
this.charts.volume = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'RX per minute',
|
||||
data: [],
|
||||
borderColor: '#4bc0c0',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.05)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'TX per minute',
|
||||
data: [],
|
||||
borderColor: '#ff6384',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.05)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: true, labels: { color: '#ffffff' } },
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const bytes = context.parsed.y;
|
||||
const formatted = self.formatBytes(bytes);
|
||||
return context.dataset.label + ': ' + formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: false,
|
||||
ticks: { color: '#b0b0b0', maxRotation: 0 },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' }
|
||||
},
|
||||
y: {
|
||||
stacked: false,
|
||||
ticks: {
|
||||
color: '#b0b0b0',
|
||||
callback: function(value) {
|
||||
return self.formatBytes(value);
|
||||
}
|
||||
},
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||
title: { display: true, text: 'Data per minute', color: '#b0b0b0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createFECChart() {
|
||||
const ctx = document.getElementById('chart-fec').getContext('2d');
|
||||
|
||||
this.charts.fec = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'FEC Corrected',
|
||||
data: [],
|
||||
borderColor: '#ffa500',
|
||||
backgroundColor: 'rgba(255, 165, 0, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'FEC Uncorrected',
|
||||
data: [],
|
||||
borderColor: '#ff0000',
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: true, labels: { color: '#ffffff' } }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#b0b0b0', maxRotation: 0 },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' }
|
||||
},
|
||||
y: {
|
||||
ticks: { color: '#b0b0b0' },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||
title: { display: true, text: 'errors/s', color: '#b0b0b0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
changeVolumeChartType(type) {
|
||||
if (!this.charts.volume) return;
|
||||
|
||||
console.log('[Charts] Changing volume chart to:', type);
|
||||
|
||||
const currentData = {
|
||||
labels: this.charts.volume.data.labels,
|
||||
datasets: this.charts.volume.data.datasets
|
||||
};
|
||||
|
||||
this.charts.volume.destroy();
|
||||
const ctx = document.getElementById('chart-volume').getContext('2d');
|
||||
const self = this;
|
||||
|
||||
const config = {
|
||||
type: type,
|
||||
data: {
|
||||
labels: currentData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'RX per minute',
|
||||
data: currentData.datasets[0].data,
|
||||
borderColor: '#4bc0c0',
|
||||
backgroundColor: type === 'bar' ? 'rgba(75, 192, 192, 0.6)' : 'rgba(75, 192, 192, 0.05)',
|
||||
borderWidth: type === 'bar' ? 1 : 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
},
|
||||
{
|
||||
label: 'TX per minute',
|
||||
data: currentData.datasets[1].data,
|
||||
borderColor: '#ff6384',
|
||||
backgroundColor: type === 'bar' ? 'rgba(255, 99, 132, 0.6)' : 'rgba(255, 99, 132, 0.05)',
|
||||
borderWidth: type === 'bar' ? 1 : 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: true, labels: { color: '#ffffff' } },
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const bytes = context.parsed.y;
|
||||
const formatted = self.formatBytes(bytes);
|
||||
return context.dataset.label + ': ' + formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: false,
|
||||
ticks: { color: '#b0b0b0', maxRotation: 0 },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' }
|
||||
},
|
||||
y: {
|
||||
stacked: false,
|
||||
ticks: {
|
||||
color: '#b0b0b0',
|
||||
callback: function(value) {
|
||||
return self.formatBytes(value);
|
||||
}
|
||||
},
|
||||
grid: { color: 'rgba(255, 255, 255, 0.1)' },
|
||||
title: { display: true, text: 'Data per minute', color: '#b0b0b0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.charts.volume = new Chart(ctx, config);
|
||||
}
|
||||
|
||||
async updateOpticalChart(period) {
|
||||
try {
|
||||
const response = await fetch('/api/history/optical/' + period);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data || !data.timestamps || data.timestamps.length === 0) {
|
||||
console.warn('[Charts] No optical data');
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = this.formatLabels(data.timestamps, period);
|
||||
|
||||
this.charts.optical.data.labels = labels;
|
||||
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.update();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Charts] Error optical:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateTemperatureChart(period) {
|
||||
try {
|
||||
const response = await fetch('/api/history/optical/' + period);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data || !data.timestamps || data.timestamps.length === 0) return;
|
||||
|
||||
const labels = this.formatLabels(data.timestamps, period);
|
||||
|
||||
this.charts.temperature.data.labels = labels;
|
||||
this.charts.temperature.data.datasets[0].data = data.data.temperature || [];
|
||||
this.charts.temperature.update();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Charts] Error temperature:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateTrafficChart(period) {
|
||||
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.datasets[0].data = rxPps;
|
||||
this.charts.traffic.data.datasets[1].data = txPps;
|
||||
this.charts.traffic.update();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Charts] Error traffic:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateBytesChart(period) {
|
||||
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 rxMbps = (data.data.rx_bytes || []).map(v =>
|
||||
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.datasets[0].data = rxMbps;
|
||||
this.charts.bytes.data.datasets[1].data = txMbps;
|
||||
this.charts.bytes.update();
|
||||
|
||||
console.log('[Charts] Bytes - max RX Mb/s:', Math.max(...rxMbps.filter(v => v !== null)).toFixed(2));
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Charts] Error bytes:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateVolumeChart(period) {
|
||||
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 rxBytesRate = data.data.rx_bytes || [];
|
||||
const txBytesRate = data.data.tx_bytes || [];
|
||||
const step = data.step || 60;
|
||||
|
||||
const rxPerInterval = rxBytesRate.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.datasets[0].data = rxPerInterval;
|
||||
this.charts.volume.data.datasets[1].data = txPerInterval;
|
||||
this.charts.volume.update();
|
||||
|
||||
const maxRx = Math.max(...rxPerInterval.filter(v => v !== null));
|
||||
console.log('[Charts] Volume - max per minute:', this.formatBytes(maxRx));
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Charts] Error volume:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateFECChart(period) {
|
||||
try {
|
||||
const response = await fetch('/api/history/fec/' + period);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data || !data.timestamps || data.timestamps.length === 0) return;
|
||||
|
||||
const labels = this.formatLabels(data.timestamps, period);
|
||||
|
||||
const correctedRate = data.data.corrected || [];
|
||||
const uncorrectedRate = data.data.uncorrected || [];
|
||||
|
||||
this.charts.fec.data.labels = labels;
|
||||
this.charts.fec.data.datasets[0].data = correctedRate;
|
||||
this.charts.fec.data.datasets[1].data = uncorrectedRate;
|
||||
this.charts.fec.update();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Charts] Error FEC:', error);
|
||||
}
|
||||
}
|
||||
|
||||
formatLabels(timestamps, period) {
|
||||
return timestamps.map(ts => {
|
||||
const date = new Date(ts * 1000);
|
||||
|
||||
if (period === '7d' || period === '14d' || period === '30d' || period === '60d' || period === '90d') {
|
||||
return date.toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit' }) + ' ' +
|
||||
date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
return date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' });
|
||||
});
|
||||
}
|
||||
|
||||
formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === null || bytes === undefined) return 'N/A';
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.charts = new GPONCharts();
|
||||
});
|
||||
256
static/js/dashboard.js
Normal file
256
static/js/dashboard.js
Normal file
@@ -0,0 +1,256 @@
|
||||
class GPONDashboard {
|
||||
constructor() {
|
||||
this.updateInterval = 5000;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.updateCurrent();
|
||||
this.updateAlerts();
|
||||
|
||||
setInterval(() => this.updateCurrent(), this.updateInterval);
|
||||
setInterval(() => this.updateAlerts(), 10000);
|
||||
|
||||
console.log('[Dashboard] Initialized');
|
||||
}
|
||||
|
||||
async updateCurrent() {
|
||||
try {
|
||||
const response = await fetch('/api/current');
|
||||
const data = await response.json();
|
||||
|
||||
console.log('[Dashboard] Data received:', data);
|
||||
|
||||
this.updateMetrics(data);
|
||||
this.updateDeviceInfo(data);
|
||||
this.updateStatus(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Dashboard] Error:', error);
|
||||
this.updateStatus({ status: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
updateMetrics(data) {
|
||||
if (data.rx_power !== undefined && data.rx_power !== null) {
|
||||
document.getElementById('rx-power').textContent = data.rx_power.toFixed(2);
|
||||
const rxPercent = ((data.rx_power + 30) / 22) * 100;
|
||||
const bar = document.getElementById('rx-power-bar');
|
||||
bar.style.width = Math.max(0, Math.min(100, rxPercent)) + '%';
|
||||
|
||||
if (data.rx_power < -28) {
|
||||
bar.className = 'progress-bar bg-danger';
|
||||
} else if (data.rx_power < -25) {
|
||||
bar.className = 'progress-bar bg-warning';
|
||||
} else {
|
||||
bar.className = 'progress-bar bg-info';
|
||||
}
|
||||
}
|
||||
|
||||
if (data.tx_power !== undefined && data.tx_power !== null) {
|
||||
document.getElementById('tx-power').textContent = data.tx_power.toFixed(2);
|
||||
const txPercent = ((data.tx_power + 5) / 10) * 100;
|
||||
document.getElementById('tx-power-bar').style.width = Math.max(0, Math.min(100, txPercent)) + '%';
|
||||
}
|
||||
|
||||
if (data.temperature !== undefined && data.temperature !== null) {
|
||||
document.getElementById('temperature').textContent = data.temperature.toFixed(1);
|
||||
const tempPercent = (data.temperature / 100) * 100;
|
||||
const bar = document.getElementById('temp-bar');
|
||||
bar.style.width = Math.max(0, Math.min(100, tempPercent)) + '%';
|
||||
|
||||
if (data.temperature > 80) {
|
||||
bar.className = 'progress-bar bg-danger';
|
||||
} else if (data.temperature > 60) {
|
||||
bar.className = 'progress-bar bg-warning';
|
||||
} else {
|
||||
bar.className = 'progress-bar bg-success';
|
||||
}
|
||||
}
|
||||
|
||||
const uptimeElem = document.getElementById('uptime');
|
||||
if (uptimeElem) {
|
||||
if (data.uptime && data.uptime > 0) {
|
||||
const seconds = parseInt(data.uptime);
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
let uptimeStr = '';
|
||||
if (days > 0) uptimeStr += days + 'd ';
|
||||
if (hours > 0 || days > 0) uptimeStr += hours + 'h ';
|
||||
uptimeStr += minutes + 'm';
|
||||
|
||||
uptimeElem.textContent = uptimeStr.trim();
|
||||
} else {
|
||||
uptimeElem.textContent = '--';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateDeviceInfo(data) {
|
||||
this.setElementText('vendor-id', data.vendor_id);
|
||||
this.setElementText('serial-number', data.serial_number);
|
||||
this.setElementText('version', data.version);
|
||||
this.setElementText('mac-address', data.mac_address);
|
||||
|
||||
this.setElementText('olt-vendor-info', data.olt_vendor_info);
|
||||
this.setElementText('olt-version-info', data.olt_version_info);
|
||||
|
||||
const connTimeElem = document.getElementById('connection-time');
|
||||
if (connTimeElem) {
|
||||
if (data.connection_time) {
|
||||
connTimeElem.textContent = data.connection_time;
|
||||
} else if (data.uptime && data.uptime > 0) {
|
||||
const seconds = parseInt(data.uptime);
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
let connTime = '';
|
||||
if (days > 0) connTime += days + 'd ';
|
||||
if (hours > 0 || days > 0) connTime += hours + 'h ';
|
||||
connTime += minutes + 'm';
|
||||
|
||||
connTimeElem.textContent = connTime.trim();
|
||||
} else {
|
||||
connTimeElem.textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
this.setElementText('rx-packets', (data.rx_packets || 0).toLocaleString('en-US'));
|
||||
this.setElementText('tx-packets', (data.tx_packets || 0).toLocaleString('en-US'));
|
||||
this.setElementText('fec-corrected', (data.fec_corrected || 0).toLocaleString('en-US'));
|
||||
this.setElementText('fec-uncorrected', (data.fec_uncorrected || 0).toLocaleString('en-US'));
|
||||
|
||||
if (data.voltage !== undefined && data.voltage !== null) {
|
||||
this.setElementText('voltage', data.voltage.toFixed(2) + ' V');
|
||||
} else {
|
||||
this.setElementText('voltage', 'N/A');
|
||||
}
|
||||
|
||||
if (data.tx_bias_current !== undefined && data.tx_bias_current !== null) {
|
||||
this.setElementText('tx-bias', data.tx_bias_current.toFixed(2) + ' mA');
|
||||
} else {
|
||||
this.setElementText('tx-bias', 'N/A');
|
||||
}
|
||||
|
||||
const volumeElem = document.getElementById('data-volume-total');
|
||||
if (volumeElem) {
|
||||
if (data.rx_bytes && data.tx_bytes) {
|
||||
const totalBytes = parseInt(data.rx_bytes) + parseInt(data.tx_bytes);
|
||||
volumeElem.textContent = this.formatBytes(totalBytes);
|
||||
} else {
|
||||
volumeElem.textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
const statusElem = document.getElementById('device-status');
|
||||
if (statusElem) {
|
||||
if (data.status === 'online') {
|
||||
statusElem.innerHTML = '<span class="badge bg-success">Online</span>';
|
||||
} else {
|
||||
statusElem.innerHTML = '<span class="badge bg-danger">Offline</span>';
|
||||
}
|
||||
}
|
||||
|
||||
const lastUpdateElem = document.getElementById('last-update');
|
||||
if (lastUpdateElem) {
|
||||
if (data.timestamp) {
|
||||
try {
|
||||
const date = new Date(data.timestamp);
|
||||
if (!isNaN(date.getTime())) {
|
||||
lastUpdateElem.textContent = date.toLocaleTimeString('en-US');
|
||||
} else {
|
||||
lastUpdateElem.textContent = '--';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Dashboard] Timestamp error:', e);
|
||||
lastUpdateElem.textContent = '--';
|
||||
}
|
||||
} else {
|
||||
lastUpdateElem.textContent = '--';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(data) {
|
||||
const indicator = document.getElementById('status-indicator');
|
||||
if (!indicator) return;
|
||||
|
||||
if (data.status === 'online') {
|
||||
indicator.innerHTML = '<i class="bi bi-circle-fill text-success"></i> Online';
|
||||
} else if (data.status === 'error') {
|
||||
indicator.innerHTML = '<i class="bi bi-circle-fill text-danger"></i> Error';
|
||||
} else {
|
||||
indicator.innerHTML = '<i class="bi bi-circle-fill text-warning"></i> Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
async updateAlerts() {
|
||||
try {
|
||||
const response = await fetch('/api/alerts');
|
||||
const alerts = await response.json();
|
||||
|
||||
const container = document.getElementById('alerts-container');
|
||||
if (!container || !alerts || alerts.length === 0) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
alerts.forEach(alert => {
|
||||
const alertDiv = document.createElement('div');
|
||||
|
||||
let alertClass = 'alert-warning';
|
||||
if (alert.severity === 'critical') alertClass = 'alert-danger';
|
||||
else if (alert.severity === 'info') alertClass = 'alert-info';
|
||||
|
||||
alertDiv.className = `alert ${alertClass} alert-dismissible fade show`;
|
||||
|
||||
const icon = alert.severity === 'critical' ? 'bi-exclamation-octagon' :
|
||||
(alert.severity === 'info' ? 'bi-info-circle' : 'bi-exclamation-triangle');
|
||||
|
||||
let timestamp = '';
|
||||
if (alert.timestamp) {
|
||||
try {
|
||||
const date = new Date(alert.timestamp);
|
||||
timestamp = date.toLocaleTimeString('en-US');
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const category = (alert.category || 'system').toUpperCase();
|
||||
|
||||
alertDiv.innerHTML = `
|
||||
<i class="bi ${icon}"></i>
|
||||
<strong>${category}:</strong> ${alert.message}
|
||||
${timestamp ? `<small class="float-end">${timestamp}</small>` : ''}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
container.appendChild(alertDiv);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Dashboard] Error updating alerts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setElementText(id, value) {
|
||||
const elem = document.getElementById(id);
|
||||
if (elem) {
|
||||
elem.textContent = (value !== undefined && value !== null && value !== '') ? value : '--';
|
||||
}
|
||||
}
|
||||
|
||||
formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.dashboard = new GPONDashboard();
|
||||
});
|
||||
Reference in New Issue
Block a user