secubox-openwrt/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/bandwidth-manager/graphs.js
CyberMind-FR ee0a7a0864 feat(bandwidth-manager): Add profiles, parental controls, alerts, traffic graphs
Major feature expansion for luci-app-bandwidth-manager:

- Device Profiles: Gaming, Streaming, IoT, Work, Kids presets with
  custom QoS settings, bandwidth limits, and latency modes
- Parental Controls: Quick preset modes (Bedtime, Homework, Family Time),
  access schedules, content filtering categories
- Bandwidth Alerts: Threshold monitoring (80/90/100%), new device alerts,
  email/SMS notifications with configurable settings
- Traffic Graphs: Real-time bandwidth charts, historical data visualization,
  top talkers list, protocol breakdown pie charts
- Time Schedules: Full CRUD with day selection, limits, priority settings

Backend additions:
- ~30 new RPCD methods for all features
- Alert monitoring cron job (every 5 minutes)
- Shared alerts.sh library for email/SMS

Frontend views:
- profiles.js, parental-controls.js, alerts.js, traffic-graphs.js
- Shared graphs.js utility for canvas drawing
- parental.css for parental controls styling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 12:25:35 +01:00

374 lines
9.8 KiB
JavaScript

'use strict';
'require baseclass';
/**
* SecuBox Graph Utilities
* Shared canvas drawing functions for bandwidth manager charts
*/
var THEME = {
cyan: '#06b6d4',
emerald: '#10b981',
rose: '#f43f5e',
amber: '#f59e0b',
violet: '#8b5cf6',
blue: '#3b82f6',
download: '#10b981',
upload: '#06b6d4',
grid: 'rgba(255,255,255,0.1)',
text: '#999',
background: '#15151a',
border: '#25252f'
};
return baseclass.extend({
/**
* Draw a smooth bezier line chart
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {Array} data - Array of {x, y} points
* @param {Object} options - Drawing options
*/
drawBezierLine: function(ctx, data, options) {
options = Object.assign({
color: THEME.cyan,
lineWidth: 2,
fill: true,
fillOpacity: 0.1,
tension: 0.3
}, options);
if (data.length < 2) return;
ctx.strokeStyle = options.color;
ctx.lineWidth = options.lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(data[0].x, data[0].y);
for (var i = 1; i < data.length; i++) {
var prev = data[i - 1];
var curr = data[i];
var cpx1 = prev.x + (curr.x - prev.x) * options.tension;
var cpy1 = prev.y;
var cpx2 = curr.x - (curr.x - prev.x) * options.tension;
var cpy2 = curr.y;
ctx.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, curr.x, curr.y);
}
ctx.stroke();
if (options.fill && options.height) {
ctx.lineTo(data[data.length - 1].x, options.height);
ctx.lineTo(data[0].x, options.height);
ctx.closePath();
var rgba = this.hexToRgba(options.color, options.fillOpacity);
ctx.fillStyle = rgba;
ctx.fill();
}
},
/**
* Draw an area chart with gradient fill
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {Array} data - Array of values
* @param {Object} options - Drawing options
*/
drawAreaChart: function(ctx, data, options) {
options = Object.assign({
color: THEME.emerald,
x: 0,
y: 0,
width: 400,
height: 200,
padding: 40
}, options);
if (data.length === 0) return;
var maxVal = Math.max.apply(null, data) || 1;
var stepX = (options.width - options.padding * 2) / (data.length - 1);
var scaleY = (options.height - options.padding * 2) / maxVal;
var points = data.map(function(val, i) {
return {
x: options.padding + i * stepX,
y: options.height - options.padding - val * scaleY
};
});
// Create gradient
var gradient = ctx.createLinearGradient(0, options.padding, 0, options.height - options.padding);
gradient.addColorStop(0, this.hexToRgba(options.color, 0.4));
gradient.addColorStop(1, this.hexToRgba(options.color, 0));
// Draw fill
ctx.beginPath();
ctx.moveTo(points[0].x, options.height - options.padding);
points.forEach(function(p) {
ctx.lineTo(p.x, p.y);
});
ctx.lineTo(points[points.length - 1].x, options.height - options.padding);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
// Draw line
this.drawBezierLine(ctx, points, {
color: options.color,
lineWidth: 2,
fill: false
});
},
/**
* Draw a bar chart
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {Array} data - Array of {label, value, color?}
* @param {Object} options - Drawing options
*/
drawBarChart: function(ctx, data, options) {
options = Object.assign({
x: 0,
y: 0,
width: 400,
height: 200,
padding: 40,
barGap: 4,
horizontal: false,
colors: [THEME.cyan, THEME.emerald, THEME.violet, THEME.amber, THEME.rose]
}, options);
if (data.length === 0) return;
var maxVal = Math.max.apply(null, data.map(function(d) { return d.value; })) || 1;
if (options.horizontal) {
var barHeight = (options.height - options.padding * 2 - options.barGap * (data.length - 1)) / data.length;
data.forEach(function(item, i) {
var barWidth = (item.value / maxVal) * (options.width - options.padding * 2 - 80);
var y = options.padding + i * (barHeight + options.barGap);
ctx.fillStyle = item.color || options.colors[i % options.colors.length];
ctx.fillRect(options.padding + 60, y, barWidth, barHeight);
// Label
ctx.fillStyle = THEME.text;
ctx.font = '12px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(item.label || '', options.padding + 55, y + barHeight / 2 + 4);
});
} else {
var barWidth = (options.width - options.padding * 2 - options.barGap * (data.length - 1)) / data.length;
data.forEach(function(item, i) {
var barHeight = (item.value / maxVal) * (options.height - options.padding * 2);
var x = options.padding + i * (barWidth + options.barGap);
ctx.fillStyle = item.color || options.colors[i % options.colors.length];
ctx.fillRect(x, options.height - options.padding - barHeight, barWidth, barHeight);
});
}
},
/**
* Draw a pie/donut chart
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {Array} data - Array of {value, color?, label?}
* @param {Object} options - Drawing options
*/
drawPieChart: function(ctx, data, options) {
options = Object.assign({
x: 200,
y: 200,
radius: 100,
innerRadius: 60,
colors: [THEME.cyan, THEME.emerald, THEME.violet, THEME.amber, THEME.rose, THEME.blue]
}, options);
if (data.length === 0) return;
var total = data.reduce(function(sum, item) { return sum + item.value; }, 0);
if (total === 0) total = 1;
var startAngle = -Math.PI / 2;
data.forEach(function(item, i) {
var sliceAngle = (item.value / total) * 2 * Math.PI;
ctx.beginPath();
ctx.moveTo(options.x, options.y);
ctx.arc(options.x, options.y, options.radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = item.color || options.colors[i % options.colors.length];
ctx.fill();
startAngle += sliceAngle;
});
// Draw inner circle for donut effect
if (options.innerRadius > 0) {
ctx.beginPath();
ctx.arc(options.x, options.y, options.innerRadius, 0, 2 * Math.PI);
ctx.fillStyle = THEME.background;
ctx.fill();
}
},
/**
* Draw chart grid lines
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {Object} options - Grid options
*/
drawGrid: function(ctx, options) {
options = Object.assign({
x: 0,
y: 0,
width: 400,
height: 200,
padding: 40,
horizontalLines: 4,
verticalLines: 0,
color: THEME.grid
}, options);
ctx.strokeStyle = options.color;
ctx.lineWidth = 1;
// Horizontal lines
for (var i = 0; i <= options.horizontalLines; i++) {
var y = options.padding + i * (options.height - options.padding * 2) / options.horizontalLines;
ctx.beginPath();
ctx.moveTo(options.padding, y);
ctx.lineTo(options.width - options.padding, y);
ctx.stroke();
}
// Vertical lines
if (options.verticalLines > 0) {
for (var j = 0; j <= options.verticalLines; j++) {
var x = options.padding + j * (options.width - options.padding * 2) / options.verticalLines;
ctx.beginPath();
ctx.moveTo(x, options.padding);
ctx.lineTo(x, options.height - options.padding);
ctx.stroke();
}
}
},
/**
* Draw axis labels
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {Array} labels - Array of label strings
* @param {Object} options - Label options
*/
drawAxisLabels: function(ctx, labels, options) {
options = Object.assign({
axis: 'x',
x: 0,
y: 0,
width: 400,
height: 200,
padding: 40,
color: THEME.text,
fontSize: 11
}, options);
ctx.fillStyle = options.color;
ctx.font = options.fontSize + 'px sans-serif';
if (options.axis === 'x') {
ctx.textAlign = 'center';
var step = (options.width - options.padding * 2) / (labels.length - 1);
labels.forEach(function(label, i) {
ctx.fillText(label, options.padding + i * step, options.height - options.padding + 15);
});
} else {
ctx.textAlign = 'right';
var step = (options.height - options.padding * 2) / (labels.length - 1);
labels.forEach(function(label, i) {
ctx.fillText(label, options.padding - 5, options.height - options.padding - i * step + 4);
});
}
},
/**
* Animate a value change
* @param {number} from - Start value
* @param {number} to - End value
* @param {number} duration - Animation duration in ms
* @param {Function} callback - Called with current value
*/
animateValue: function(from, to, duration, callback) {
var start = performance.now();
var diff = to - from;
function step(timestamp) {
var progress = Math.min((timestamp - start) / duration, 1);
var eased = 1 - Math.pow(1 - progress, 3); // easeOutCubic
var current = from + diff * eased;
callback(current);
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
},
/**
* Convert hex color to rgba
* @param {string} hex - Hex color
* @param {number} alpha - Alpha value 0-1
* @returns {string} RGBA color string
*/
hexToRgba: function(hex, alpha) {
var r = parseInt(hex.slice(1, 3), 16);
var g = parseInt(hex.slice(3, 5), 16);
var b = parseInt(hex.slice(5, 7), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
},
/**
* Format bytes to human readable
* @param {number} bytes - Byte count
* @returns {string} Formatted string
*/
formatBytes: function(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
/**
* Format bits per second
* @param {number} bps - Bits per second
* @returns {string} Formatted string
*/
formatBps: function(bps) {
if (bps === 0) return '0 bps';
var k = 1000;
var sizes = ['bps', 'Kbps', 'Mbps', 'Gbps'];
var i = Math.floor(Math.log(bps) / Math.log(k));
return parseFloat((bps / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
/**
* Get theme colors
* @returns {Object} Theme color object
*/
getTheme: function() {
return THEME;
}
});