Merge branch 'develop' into insightful_ui

# Conflicts:
#	src/rust/lqosd/src/node_manager/static2/node_manager.css
This commit is contained in:
Herbert Wolverson 2025-02-04 08:40:02 -06:00
commit 011b2ce971
54 changed files with 3749 additions and 75 deletions

View File

@ -53,3 +53,6 @@ You can enroll in the 30-day free trial by [upgrading to the latest version of L
<img alt="LibreQoS Long Term Stats" src="https://i0.wp.com/libreqos.io/wp-content/uploads/2023/11/01-Dashboard.png"></a>
## External Pull Request Policy
We can only accept PRs that address one specific change or topic each. We ask that you keep all changes small and focused per-PR to help our review and testing process.

View File

@ -1 +1 @@
1.5-BETA7
1.5-BETA8

View File

@ -20,7 +20,7 @@ pub async fn bus_request(requests: Vec<BusRequest>) -> Result<Vec<BusResponse>,
let stream = UnixStream::connect(BUS_SOCKET_PATH).await;
if let Err(e) = &stream {
if e.kind() == std::io::ErrorKind::NotFound {
error!("Unable to access {BUS_SOCKET_PATH}. Check that lqosd is running and you have appropriate permissions.");
//error!("Unable to access {BUS_SOCKET_PATH}. Check that lqosd is running and you have appropriate permissions.");
return Err(BusClientError::SocketNotFound);
}
}

View File

@ -33,6 +33,17 @@ impl From<&str> for UserRole {
}
}
impl From<String> for UserRole {
fn from(s: String) -> Self {
let s = s.to_lowercase();
if s == "admin" {
UserRole::Admin
} else {
UserRole::ReadOnly
}
}
}
impl Display for UserRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -43,11 +54,11 @@ impl Display for UserRole {
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct WebUser {
username: String,
password_hash: String,
role: UserRole,
token: String,
pub struct WebUser {
pub username: String,
pub password_hash: String,
pub role: UserRole,
pub token: String,
}
/// Container holding the authorized web users.
@ -237,6 +248,11 @@ impl WebUsers {
Ok(())
}
/// Return a list of user objects
pub fn get_users(&self) -> Vec<WebUser> {
self.users.clone()
}
/// Sets the "allow unauthenticated users" field. If true,
/// unauthenticated users gain read-only access. This is useful
/// for demonstration purposes.

View File

@ -1,9 +1,14 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use ip_network::IpNetwork;
use ip_network_table::IpNetworkTable;
use serde::{Serialize, Deserialize};
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct IpRanges {
pub ignore_subnets: Vec<String>,
pub allow_subnets: Vec<String>,
pub unknown_ip_honors_ignore: Option<bool>,
pub unknown_ip_honors_allow: Option<bool>,
}
impl Default for IpRanges {
@ -16,6 +21,50 @@ impl Default for IpRanges {
"100.64.0.0/10".to_string(),
"192.168.0.0/16".to_string(),
],
unknown_ip_honors_ignore: Some(true),
unknown_ip_honors_allow: Some(true),
}
}
}
impl IpRanges {
/// Maps the ignored IP ranges to an LPM table.
pub fn ignored_network_table(&self) -> IpNetworkTable<bool> {
let mut ignored = IpNetworkTable::new();
for excluded_ip in self.ignore_subnets.iter() {
let split: Vec<_> = excluded_ip.split('/').collect();
if split[0].contains(':') {
// It's IPv6
let ip_network: Ipv6Addr = split[0].parse().unwrap();
let ip = IpNetwork::new(ip_network, split[1].parse().unwrap()).unwrap();
ignored.insert(ip, true);
} else {
// It's IPv4
let ip_network: Ipv4Addr = split[0].parse().unwrap();
let ip = IpNetwork::new(ip_network, split[1].parse().unwrap()).unwrap();
ignored.insert(ip, true);
}
}
ignored
}
/// Maps the allowed IP ranges to an LPM table.
pub fn allowed_network_table(&self) -> IpNetworkTable<bool> {
let mut allowed = IpNetworkTable::new();
for allowed_ip in self.allow_subnets.iter() {
let split: Vec<_> = allowed_ip.split('/').collect();
if split[0].contains(':') {
// It's IPv6
let ip_network: Ipv6Addr = split[0].parse().unwrap();
let ip = IpNetwork::new(ip_network, split[1].parse().unwrap()).unwrap();
allowed.insert(ip, true);
} else {
// It's IPv4
let ip_network: Ipv4Addr = split[0].parse().unwrap();
let ip = IpNetwork::new(ip_network, split[1].parse().unwrap()).unwrap();
allowed.insert(ip, true);
}
}
allowed
}
}

View File

@ -12,7 +12,7 @@ mod network_json;
mod program_control;
mod shaped_devices;
pub use authentication::{UserRole, WebUsers};
pub use authentication::{UserRole, WebUsers, WebUser};
pub use etc::{load_config, Config, enable_long_term_stats, Tunables, BridgeConfig, update_config, disable_xdp_bridge};
pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport};
pub use program_control::load_libreqos;

View File

@ -6,6 +6,7 @@ use std::sync::{Arc, Mutex};
use std::sync::mpsc::Sender;
use std::time::Duration;
use timerfd::{SetTimeFlags, TimerFd, TimerState};
use tracing::info;
pub(crate) use permission::check_submit_permission;
use crate::lts2_sys::lts2_client::ingestor::commands::IngestorCommand;
use crate::lts2_sys::lts2_client::ingestor::message_queue::MessageQueue;
@ -26,12 +27,12 @@ fn ingestor_loop(
let my_message_queue = message_queue.clone();
std::thread::spawn(move || ticker_timer(my_message_queue));
println!("Starting ingestor loop");
info!("Starting ingestor loop");
while let Ok(msg) = rx.recv() {
let mut message_queue_lock = message_queue.lock().unwrap();
message_queue_lock.ingest(msg);
}
println!("Ingestor loop exited");
info!("Ingestor loop exited");
}
fn ticker_timer(message_queue: Arc<Mutex<MessageQueue>>) {
@ -46,7 +47,7 @@ fn ticker_timer(message_queue: Arc<Mutex<MessageQueue>>) {
loop {
let missed_ticks = tfd.read();
if missed_ticks > 1 {
println!("Missed queue submission ticks: {}", missed_ticks - 1);
info!("Missed queue submission ticks: {}", missed_ticks - 1);
}
let permitted = is_allowed_to_submit();
@ -54,11 +55,11 @@ fn ticker_timer(message_queue: Arc<Mutex<MessageQueue>>) {
if !message_queue_lock.is_empty() && permitted {
let start = std::time::Instant::now();
if let Err(e) = message_queue_lock.send() {
println!("Failed to send queue: {e:?}");
info!("Failed to send queue: {e:?}");
}
println!("Queue send took: {:?}s", start.elapsed().as_secs_f32());
info!("Queue send took: {:?}s", start.elapsed().as_secs_f32());
} else {
println!("Queue is empty or not permitted to send - nothing to do");
info!("Queue is empty or not permitted to send - nothing to do");
}
}
}

View File

@ -1,6 +1,6 @@
#!/bin/bash
set -e
scripts=( index.js template.js login.js first-run.js shaped-devices.js tree.js help.js unknown-ips.js configuration.js circuit.js flow_map.js all_tree_sankey.js asn_explorer.js lts_trial.js )
scripts=( index.js template.js login.js first-run.js shaped-devices.js tree.js help.js unknown-ips.js configuration.js circuit.js flow_map.js all_tree_sankey.js asn_explorer.js lts_trial.js config_general.js config_anon.js config_tuning.js config_queues.js config_lts.js config_iprange.js config_flows.js config_integration.js config_spylnx.js config_uisp.js config_powercode.js config_sonar.js config_interface.js config_network.js config_devices.js config_users.js )
for script in "${scripts[@]}"
do
echo "Building {$script}"

View File

@ -0,0 +1,135 @@
export function loadConfig(onComplete) {
$.get("/local-api/getConfig", (data) => {
window.config = data;
onComplete();
});
}
export function saveConfig(onComplete) {
$.ajax({
type: "POST",
url: "/local-api/updateConfig",
data: JSON.stringify(window.config),
contentType: 'application/json',
success: () => {
onComplete();
},
error: () => {
alert("That didn't work");
}
});
}
export function saveNetworkAndDevices(network_json, shaped_devices, onComplete) {
// Validate network_json structure
if (!network_json || typeof network_json !== 'object') {
alert("Invalid network configuration");
return;
}
// Validate shaped_devices structure
if (!Array.isArray(shaped_devices)) {
alert("Invalid shaped devices configuration");
return;
}
// Validate individual shaped devices
const validationErrors = [];
const validNodes = validNodeList(network_json);
console.log(validNodes);
shaped_devices.forEach((device, index) => {
// Required fields
if (!device.circuit_id || device.circuit_id.trim() === "") {
validationErrors.push(`Device ${index + 1}: Circuit ID is required`);
}
if (!device.device_id || device.device_id.trim() === "") {
validationErrors.push(`Device ${index + 1}: Device ID is required`);
}
// Parent node validation
if (device.parent_node && validNodes.length > 0 && !validNodes.includes(device.parent_node)) {
validationErrors.push(`Device ${index + 1}: Parent node '${device.parent_node}' does not exist`);
}
// Bandwidth validation
if (device.download_min_mbps < 1 || device.upload_min_mbps < 1 ||
device.download_max_mbps < 1 || device.upload_max_mbps < 1) {
validationErrors.push(`Device ${index + 1}: Bandwidth values must be greater than 0`);
}
});
if (validationErrors.length > 0) {
alert("Validation errors:\n" + validationErrors.join("\n"));
return;
}
// Prepare data for submission
const submission = {
network_json,
shaped_devices
};
console.log(submission);
// Send to server with enhanced error handling
/*$.ajax({
type: "POST",
url: "/local-api/updateNetworkAndDevices",
contentType: 'application/json',
data: JSON.stringify(submission),
dataType: 'json', // Expect JSON response
success: (response) => {
try {
if (response && response.success) {
if (onComplete) onComplete(true, "Saved successfully");
} else {
const msg = response?.message || "Unknown error occurred";
if (onComplete) onComplete(false, msg);
alert("Failed to save: " + msg);
}
} catch (e) {
console.error("Error parsing response:", e);
if (onComplete) onComplete(false, "Invalid server response");
alert("Invalid server response format");
}
},
error: (xhr) => {
let errorMsg = "Request failed";
try {
if (xhr.responseText) {
const json = JSON.parse(xhr.responseText);
errorMsg = json.message || xhr.responseText;
} else if (xhr.statusText) {
errorMsg = xhr.statusText;
}
console.error("AJAX Error:", {
status: xhr.status,
statusText: xhr.statusText,
response: xhr.responseText
});
} catch (e) {
console.error("Error parsing error response:", e);
errorMsg = "Unknown error occurred";
}
if (onComplete) onComplete(false, errorMsg);
alert("Error saving configuration: " + errorMsg);
}
});*/
}
export function validNodeList(network_json) {
let nodes = [];
function iterate(data, level) {
for (const [key, value] of Object.entries(data)) {
nodes.push(key);
if (value.children != null)
iterate(value.children, level+1);
}
}
iterate(network_json, 0);
return nodes;
}

View File

@ -0,0 +1,42 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate server address format if provided
const server = document.getElementById("anonymousServer").value.trim();
if (server) {
const parts = server.split(':');
if (parts.length !== 2 || isNaN(parseInt(parts[1]))) {
alert("Statistics Server must be in format HOST:PORT");
return false;
}
}
return true;
}
function updateConfig() {
// Update only the usage stats section
window.config.usage_stats.send_anonymous = document.getElementById("sendAnonymous").checked;
window.config.usage_stats.anonymous_server = document.getElementById("anonymousServer").value.trim();
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.usage_stats) {
// Required fields
document.getElementById("sendAnonymous").checked = window.config.usage_stats.send_anonymous ?? true;
document.getElementById("anonymousServer").value = window.config.usage_stats.anonymous_server ?? "stats.libreqos.io:9125";
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Usage statistics configuration not found in window.config");
}
});

View File

@ -0,0 +1,477 @@
let shaped_devices = null;
let network_json = null;
function start() {
// Load shaped devices data
$.get("/local-api/allShapedDevices", (data) => {
shaped_devices = data;
// Load network data
$.get("/local-api/networkJson", (njs) => {
network_json = njs;
shapedDevices();
});
});
// Setup button handlers
$("#btnNewDevice").on('click', newSdRow);
window.deleteSdRow = deleteSdRow;
}
function rowPrefix(rowId, boxId) {
return "sdr_" + rowId + "_" + boxId;
}
function makeSheetBox(rowId, boxId, value, small=false) {
let html = "";
if (!small) {
html = "<td style='padding: 0px'><input id='" + rowPrefix(rowId, boxId) + "' type=\"text\" value=\"" + value + "\"></input></td>"
} else {
html = "<td style='padding: 0px'><input id='" + rowPrefix(rowId, boxId) + "' type=\"text\" value=\"" + value + "\" style='font-size: 8pt;'></input></td>"
}
return html;
}
function makeSheetNumberBox(rowId, boxId, value) {
let html = "<td style='padding: 0px'><input id='" + rowPrefix(rowId, boxId) + "' type=\"number\" value=\"" + value + "\" style='width: 100px; font-size: 8pt;'></input></td>"
return html;
}
function separatedIpArray(rowId, boxId, value) {
let html = "<td style='padding: 0px'>";
let val = "";
for (i = 0; i<value.length; i++) {
val += value[i][0];
val += "/";
val += value[i][1];
val += ", ";
}
if (val.length > 0) {
val = val.substring(0, val.length-2);
}
html += "<input id='" + rowPrefix(rowId, boxId) + "' type='text' style='font-size: 8pt; width: 100px;' value='" + val + "'></input>";
html += "</td>";
return html;
}
function nodeDropDown(rowId, boxId, selectedNode) {
let html = "<td style='padding: 0px'>";
html += "<select id='" + rowPrefix(rowId, boxId) + "' style='font-size: 8pt; width: 150px;'>";
function iterate(data, level) {
let html = "";
for (const [key, value] of Object.entries(data)) {
html += "<option value='" + key + "'";
if (key === selectedNode) html += " selected";
html += ">";
for (let i=0; i<level; i++) html += "-";
html += key;
html += "</option>";
if (value.children != null)
html += iterate(value.children, level+1);
}
return html;
}
html += iterate(network_json, 0);
html += "</select>";
html += "</td>";
return html;
}
function newSdRow() {
shaped_devices.unshift({
circuit_id: "new_circuit",
circuit_name: "new circuit",
device_id: "new_device",
device_name: "new device",
mac: "",
ipv4: "",
ipv6: "",
download_min_mbps: 100,
upload_min_mbps: 100,
download_max_mbps: 100,
upload_max_mbps: 100,
comment: "",
});
shapedDevices();
}
function deleteSdRow(id) {
shaped_devices.splice(id, 1);
shapedDevices();
}
function shapedDevices() {
console.log(shaped_devices);
let html = "<table style='height: 500px; overflow: scroll; border-collapse: collapse; width: 100%; padding: 0px'>";
html += "<thead style='position: sticky; top: 0; height: 50px; background: navy; color: white;'>";
html += "<tr style='font-size: 9pt;'><th>Circuit ID</th><th>Circuit Name</th><th>Device ID</th><th>Device Name</th><th>Parent Node</th><th>MAC</th><th>IPv4</th><th>IPv6</th><th>Download Min</th><th>Upload Min</th><th>Download Max</th><th>Upload Max</th><th>Comment</th><th></th></th></tr>";
html += "</thead>";
for (var i=0; i<shaped_devices.length; i++) {
let row = shaped_devices[i];
html += "<tr>";
html += makeSheetBox(i, "circuit_id", row.circuit_id, true);
html += makeSheetBox(i, "circuit_name", row.circuit_name, true);
html += makeSheetBox(i, "device_id", row.device_id, true);
html += makeSheetBox(i, "device_name", row.device_name, true);
html += nodeDropDown(i, "parent_node", row.parent_node, true);
html += makeSheetBox(i, "mac", row.mac, true);
html += separatedIpArray(i, "ipv4", row.ipv4);
html += separatedIpArray(i, "ipv6", row.ipv6);
html += makeSheetNumberBox(i, "download_min_mbps", row.download_min_mbps);
html += makeSheetNumberBox(i, "upload_min_mbps", row.upload_min_mbps);
html += makeSheetNumberBox(i, "download_max_mbps", row.download_max_mbps);
html += makeSheetNumberBox(i, "upload_max_mbps", row.upload_max_mbps);
html += makeSheetBox(i, "comment", row.comment, true);
html += "<td><button class='btn btn-sm btn-secondary' type='button' onclick='window.deleteSdRow(" + i + ")'><i class='fa fa-trash'></i></button></td>";
html += "</tr>";
}
html += "</tbody></table>";
$("#shapedDeviceTable").html(html);
}
function start() {
// Load shaped devices data
$.get("/local-api/networkJson", (njs) => {
network_json = njs;
$.get("/local-api/allShapedDevices", (data) => {
shaped_devices = data;
shapedDevices();
});
});
// Setup button handlers
$("#btnNewDevice").on('click', newSdRow);
$("#btnSaveDevices").on('click', () => {
// Validate before saving
const validation = validateSd();
if (!validation.valid) {
alert("Cannot save - please fix validation errors first");
return;
}
// Update shaped devices from UI
for (let i=0; i<shaped_devices.length; i++) {
let row = shaped_devices[i];
row.circuit_id = $("#" + rowPrefix(i, "circuit_id")).val();
row.circuit_name = $("#" + rowPrefix(i, "circuit_name")).val();
row.device_id = $("#" + rowPrefix(i, "device_id")).val();
row.device_name = $("#" + rowPrefix(i, "device_name")).val();
row.parent_node = $("#" + rowPrefix(i, "parent_node")).val();
row.mac = $("#" + rowPrefix(i, "mac")).val();
row.ipv4 = ipAddressesToTuple($("#" + rowPrefix(i, "ipv4")).val());
row.ipv6 = ipAddressesToTuple($("#" + rowPrefix(i, "ipv6")).val());
row.download_min_mbps = parseInt($("#" + rowPrefix(i, "download_min_mbps")).val());
row.upload_min_mbps = parseInt($("#" + rowPrefix(i, "upload_min_mbps")).val());
row.download_max_mbps = parseInt($("#" + rowPrefix(i, "download_max_mbps")).val());
row.upload_max_mbps = parseInt($("#" + rowPrefix(i, "upload_max_mbps")).val());
row.comment = $("#" + rowPrefix(i, "comment")).val();
}
saveNetworkAndDevices(network_json, shaped_devices, (success, message) => {
if (success) {
alert("Configuration saved successfully!");
} else {
alert("Failed to save configuration: " + message);
}
});
});
window.deleteSdRow = deleteSdRow;
}
function validateSd() {
let valid = true;
let errors = [];
$(".invalid").removeClass("invalid");
let validNodes = validNodeList();
for (let i=0; i<shaped_devices.length; i++) {
// Check that circuit ID is good
let controlId = "#" + rowPrefix(i, "circuit_id");
let circuit_id = $(controlId).val();
if (circuit_id.length === 0) {
valid = false;
errors.push("Circuits must have a Circuit ID");
$(controlId).addClass("invalid");
}
// Check that the Circuit Name is good
controlId = "#" + rowPrefix(i, "circuit_name");
let circuit_name = $(controlId).val();
if (circuit_name.length === 0) {
valid = false;
errors.push("Circuits must have a Circuit Name");
$(controlId).addClass("invalid");
}
// Check that the Device ID is good
controlId = "#" + rowPrefix(i, "device_id");
let device_id = $(controlId).val();
if (device_id.length === 0) {
valid = false;
errors.push("Circuits must have a Device ID");
$(controlId).addClass("invalid");
}
for (let j=0; j<shaped_devices.length; j++) {
if (i !== j) {
if (shaped_devices[j].device_id === device_id) {
valid = false;
errors.push("Devices with duplicate ID [" + device_id + "] detected");
$(controlId).addClass("invalid");
$("#" + rowPrefix(j, "device_id")).addClass("invalid");
}
}
}
// Check that the Device Name is good
controlId = "#" + rowPrefix(i, "device_name");
let device_name = $(controlId).val();
if (device_name.length === 0) {
valid = false;
errors.push("Circuits must have a Device Name");
$(controlId).addClass("invalid");
}
// Check the parent node
controlId = "#" + rowPrefix(i, "parent_node");
let parent_node = $(controlId).val();
if (parent_node == null) parent_node = "";
if (validNodes.length === 0) {
// Flat
if (parent_node.length > 0) {
valid = false;
errors.push("You have a flat network, so you can't specify a parent node.");
$(controlId).addClass("invalid");
}
} else {
// Hierarchy - so we need to know if it exists
if (validNodes.indexOf(parent_node) === -1) {
valid = false;
errors.push("Parent node: " + parent_node + " does not exist");
$(controlId).addClass("invalid");
}
}
// We can ignore the MAC address
// IPv4
controlId = "#" + rowPrefix(i, "ipv4");
let ipv4 = $(controlId).val();
if (ipv4.length > 0) {
// We have IP addresses
if (ipv4.indexOf(',') !== -1) {
// We have multiple addresses
let ips = ipv4.replace(' ', '').split(',');
for (let j=0; j<ips.length; j++) {
if (!checkIpv4(ips[j].trim())) {
valid = false;
errors.push(ips[j] + "is not a valid IPv4 address");
$(controlId).addClass("invalid");
}
let dupes = checkIpv4Duplicate(ips[j], i);
if (dupes > 0 && dupes !== i) {
valid = false;
errors.push(ips[j] + " is a duplicate IP");
$(controlId).addClass("invalid");
$("#" + rowPrefix(dupes, "ipv4")).addClass("invalid");
}
}
} else {
// Just the one
if (!checkIpv4(ipv4)) {
valid = false;
errors.push(ipv4 + "is not a valid IPv4 address");
$(controlId).addClass("invalid");
}
let dupes = checkIpv4Duplicate(ipv4, i);
if (dupes > 0) {
valid = false;
errors.push(ipv4 + " is a duplicate IP");
$(controlId).addClass("invalid");
$("#" + rowPrefix(dupes, "ipv4")).addClass("invalid");
}
}
}
// IPv6
controlId = "#" + rowPrefix(i, "ipv6");
let ipv6 = $(controlId).val();
if (ipv6.length > 0) {
// We have IP addresses
if (ipv6.indexOf(',') !== -1) {
// We have multiple addresses
let ips = ipv6.replace(' ', '').split(',');
for (let j=0; j<ips.length; j++) {
if (!checkIpv6(ips[j].trim())) {
valid = false;
errors.push(ips[j] + "is not a valid IPv6 address");
$(controlId).addClass("invalid");
}
let dupes = checkIpv6Duplicate(ips[j], i);
if (dupes > 0 && dupes !== i) {
valid = false;
errors.push(ips[j] + " is a duplicate IP");
$(controlId).addClass("invalid");
$("#" + rowPrefix(dupes, "ipv6")).addClass("invalid");
}
}
} else {
// Just the one
if (!checkIpv6(ipv6)) {
valid = false;
errors.push(ipv6 + "is not a valid IPv6 address");
$(controlId).addClass("invalid");
}
let dupes = checkIpv6Duplicate(ipv6, i);
if (dupes > 0 && dupes !== i) {
valid = false;
errors.push(ipv6 + " is a duplicate IP");
$(controlId).addClass("invalid");
$("#" + rowPrefix(dupes, "ipv6")).addClass("invalid");
}
}
}
// Combined - must be an address between them
if (ipv4.length === 0 && ipv6.length === 0) {
valid = false;
errors.push("You must specify either an IPv4 or IPv6 (or both) address");
$(controlId).addClass("invalid");
$("#" + rowPrefix(i, "ipv4")).addClass("invalid");
}
// Download Min
controlId = "#" + rowPrefix(i, "download_min_mbps");
let download_min = $(controlId).val();
download_min = parseInt(download_min);
if (isNaN(download_min)) {
valid = false;
errors.push("Download min is not a valid number");
$(controlId).addClass("invalid");
} else if (download_min < 1) {
valid = false;
errors.push("Download min must be 1 or more");
$(controlId).addClass("invalid");
}
// Upload Min
controlId = "#" + rowPrefix(i, "upload_min_mbps");
let upload_min = $(controlId).val();
upload_min = parseInt(upload_min);
if (isNaN(upload_min)) {
valid = false;
errors.push("Upload min is not a valid number");
$(controlId).addClass("invalid");
} else if (upload_min < 1) {
valid = false;
errors.push("Upload min must be 1 or more");
$(controlId).addClass("invalid");
}
// Download Max
controlId = "#" + rowPrefix(i, "download_max_mbps");
let download_max = $(controlId).val();
upload_min = parseInt(download_max);
if (isNaN(download_max)) {
valid = false;
errors.push("Download Max is not a valid number");
$(controlId).addClass("invalid");
} else if (download_max < 1) {
valid = false;
errors.push("Download Max must be 1 or more");
$(controlId).addClass("invalid");
}
// Upload Max
controlId = "#" + rowPrefix(i, "upload_max_mbps");
let upload_max = $(controlId).val();
upload_min = parseInt(upload_max);
if (isNaN(upload_max)) {
valid = false;
errors.push("Upload Max is not a valid number");
$(controlId).addClass("invalid");
} else if (upload_max < 1) {
valid = false;
errors.push("Upload Max must be 1 or more");
$(controlId).addClass("invalid");
}
}
if (!valid) {
let errorMessage = "Invalid ShapedDevices Entries:\n";
for (let i=0; i<errors.length; i++) {
errorMessage += errors[i] + "\n";
}
alert(errorMessage);
}
return {
valid: valid,
errors: errors
};
}
function checkIpv4(ip) {
const ipv4Pattern =
/^(\d{1,3}\.){3}\d{1,3}$/;
if (ip.indexOf('/') === -1) {
return ipv4Pattern.test(ip);
} else {
let parts = ip.split('/');
return ipv4Pattern.test(parts[0]);
}
}
function checkIpv6(ip) {
// Check if the input is a valid IPv6 address with prefix
const regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]{1,3}))?$/;
return regex.test(ip);
}
function checkIpv4Duplicate(ip, index) {
ip = ip.trim();
for (let i=0; i < shaped_devices.length; i++) {
if (i !== index) {
let sd = shaped_devices[i];
for (let j=0; j<sd.ipv4.length; j++) {
let formatted = "";
if (ip.indexOf('/') > 0) {
formatted = sd.ipv4[j][0] + "/" + sd.ipv4[j][1];
} else {
formatted = sd.ipv4[j][0];
}
if (formatted === ip) {
return index;
}
}
}
}
return -1;
}
function checkIpv6Duplicate(ip, index) {
ip = ip.trim();
for (let i=0; i < shaped_devices.length; i++) {
if (i !== index) {
let sd = shaped_devices[i];
for (let j=0; j<sd.ipv6.length; j++) {
let formatted = "";
if (ip.indexOf('/') > 0) {
formatted = sd.ipv6[j][0] + "/" + sd.ipv6[j][1];
} else {
formatted = sd.ipv6[j][0];
}
if (formatted === ip) {
return index;
}
}
}
}
return -1;
}
$(document).ready(start);

View File

@ -0,0 +1,93 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function populateDoNotTrackList(selectId, subnets) {
const select = document.getElementById(selectId);
select.innerHTML = ''; // Clear existing options
if (subnets) {
subnets.forEach(subnet => {
const option = document.createElement('option');
option.value = subnet;
option.text = subnet;
select.appendChild(option);
});
}
}
function validateConfig() {
// Validate required fields
const flowTimeout = parseInt(document.getElementById("flowTimeout").value);
if (isNaN(flowTimeout) || flowTimeout < 1) {
alert("Flow Timeout must be a number greater than 0");
return false;
}
// Validate optional fields if provided
const netflowPort = document.getElementById("netflowPort").value;
if (netflowPort && (isNaN(netflowPort) || netflowPort < 1 || netflowPort > 65535)) {
alert("Netflow Port must be a number between 1 and 65535");
return false;
}
const netflowIp = document.getElementById("netflowIp").value.trim();
if (netflowIp) {
try {
new URL(`http://${netflowIp}`);
} catch {
alert("Netflow IP must be a valid IP address");
return false;
}
}
return true;
}
function updateConfig() {
// Update only the flows section
window.config.flows = {
flow_timeout_seconds: parseInt(document.getElementById("flowTimeout").value),
netflow_enabled: document.getElementById("enableNetflow").checked,
netflow_port: document.getElementById("netflowPort").value ?
parseInt(document.getElementById("netflowPort").value) : null,
netflow_ip: document.getElementById("netflowIp").value.trim() || null,
netflow_version: document.getElementById("netflowVersion").value ?
parseInt(document.getElementById("netflowVersion").value) : null,
do_not_track_subnets: getSubnetsFromList('doNotTrackSubnets')
};
}
function getSubnetsFromList(listId) {
const select = document.getElementById(listId);
return Array.from(select.options).map(option => option.value);
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.flows) {
const flows = window.config.flows;
// Required fields
document.getElementById("flowTimeout").value = flows.flow_timeout_seconds;
document.getElementById("enableNetflow").checked = flows.netflow_enabled ?? false;
// Optional fields
document.getElementById("netflowPort").value = flows.netflow_port ?? "";
document.getElementById("netflowIP").value = flows.netflow_ip ?? "";
document.getElementById("netflowVersion").value = flows.netflow_version ?? "5";
// Populate do not track list
populateDoNotTrackList('doNotTrackSubnets', flows.do_not_track_subnets);
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Flows configuration not found in window.config");
}
});

View File

@ -0,0 +1,81 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate required fields
const nodeName = document.getElementById("nodeName").value.trim();
if (!nodeName) {
alert("Node Name is required");
return false;
}
const packetCaptureTime = parseInt(document.getElementById("packetCaptureTime").value);
if (isNaN(packetCaptureTime) || packetCaptureTime < 1) {
alert("Packet Capture Time must be a number greater than 0");
return false;
}
const queueCheckPeriod = parseInt(document.getElementById("queueCheckPeriod").value);
if (isNaN(queueCheckPeriod) || queueCheckPeriod < 100) {
alert("Queue Check Period must be a number of at least 100 milliseconds");
return false;
}
// Validate webserver listen address if provided
const webserverListen = document.getElementById("webserverListen").value.trim();
if (webserverListen) {
const parts = webserverListen.split(':');
if (parts.length !== 2 || isNaN(parseInt(parts[1]))) {
alert("Web Server Listen Address must be in format IP:PORT");
return false;
}
}
return true;
}
function updateConfig() {
// Update only the general configuration section
window.config.node_name = document.getElementById("nodeName").value.trim();
window.config.packet_capture_time = parseInt(document.getElementById("packetCaptureTime").value);
window.config.queue_check_period_ms = parseInt(document.getElementById("queueCheckPeriod").value);
window.config.disable_webserver = document.getElementById("disableWebserver").checked;
const webserverListen = document.getElementById("webserverListen").value.trim();
window.config.webserver_listen = webserverListen ? webserverListen : null;
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config) {
// Required fields
if (window.config.node_id) {
document.getElementById("nodeId").value = window.config.node_id;
}
if (window.config.node_name) {
document.getElementById("nodeName").value = window.config.node_name;
}
if (window.config.packet_capture_time) {
document.getElementById("packetCaptureTime").value = window.config.packet_capture_time;
}
if (window.config.queue_check_period_ms) {
document.getElementById("queueCheckPeriod").value = window.config.queue_check_period_ms;
}
// Optional fields with nullish coalescing
document.getElementById("disableWebserver").checked = window.config.disable_webserver ?? false;
document.getElementById("webserverListen").value = window.config.webserver_listen ?? "";
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Configuration not found in window.config");
}
});

View File

@ -0,0 +1,50 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate queue refresh interval
const interval = parseInt(document.getElementById("queueRefreshInterval").value);
if (isNaN(interval) || interval < 1) {
alert("Queue Refresh Interval must be a number greater than 0");
return false;
}
return true;
}
function updateConfig() {
// Update only the integration_common section
window.config.integration_common = {
circuit_name_as_address: document.getElementById("circuitNameAsAddress").checked,
always_overwrite_network_json: document.getElementById("alwaysOverwriteNetworkJson").checked,
queue_refresh_interval_mins: parseInt(document.getElementById("queueRefreshInterval").value)
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.integration_common) {
const integration = window.config.integration_common;
// Boolean fields
document.getElementById("circuitNameAsAddress").checked =
integration.circuit_name_as_address ?? false;
document.getElementById("alwaysOverwriteNetworkJson").checked =
integration.always_overwrite_network_json ?? false;
// Numeric field
document.getElementById("queueRefreshInterval").value =
integration.queue_refresh_interval_mins ?? 30;
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Integration configuration not found in window.config");
}
});

View File

@ -0,0 +1,101 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
if (document.getElementById('bridgeMode').checked) {
// Validate bridge mode fields
const toInternet = document.getElementById('toInternet').value.trim();
const toNetwork = document.getElementById('toNetwork').value.trim();
if (!toInternet || !toNetwork) {
alert("Both interface names are required in bridge mode");
return false;
}
} else if (document.getElementById('singleInterfaceMode').checked) {
// Validate single interface mode fields
const interfaceName = document.getElementById('interface').value.trim();
const internetVlan = parseInt(document.getElementById('internetVlan').value);
const networkVlan = parseInt(document.getElementById('networkVlan').value);
if (!interfaceName) {
alert("Interface name is required in single interface mode");
return false;
}
if (isNaN(internetVlan) || internetVlan < 1 || internetVlan > 4094) {
alert("Internet VLAN must be between 1 and 4094");
return false;
}
if (isNaN(networkVlan) || networkVlan < 1 || networkVlan > 4094) {
alert("Network VLAN must be between 1 and 4094");
return false;
}
} else {
alert("Please select either bridge or single interface mode");
return false;
}
return true;
}
function updateConfig() {
// Clear both sections first
window.config.bridge = null;
window.config.single_interface = null;
if (document.getElementById('bridgeMode').checked) {
// Update bridge configuration
window.config.bridge = {
use_xdp_bridge: document.getElementById('useXdpBridge').checked,
to_internet: document.getElementById('toInternet').value.trim(),
to_network: document.getElementById('toNetwork').value.trim()
};
} else {
// Update single interface configuration
window.config.single_interface = {
interface: document.getElementById('interface').value.trim(),
internet_vlan: parseInt(document.getElementById('internetVlan').value),
network_vlan: parseInt(document.getElementById('networkVlan').value)
};
}
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config) {
// Determine which mode is active
if (window.config.bridge) {
// Bridge mode
document.getElementById('bridgeMode').checked = true;
document.getElementById('useXdpBridge').checked =
window.config.bridge.use_xdp_bridge ?? true;
document.getElementById('toInternet').value =
window.config.bridge.to_internet ?? "eth0";
document.getElementById('toNetwork').value =
window.config.bridge.to_network ?? "eth1";
} else if (window.config.single_interface) {
// Single interface mode
document.getElementById('singleInterfaceMode').checked = true;
document.getElementById('interface').value =
window.config.single_interface.interface ?? "eth0";
document.getElementById('internetVlan').value =
window.config.single_interface.internet_vlan ?? 2;
document.getElementById('networkVlan').value =
window.config.single_interface.network_vlan ?? 3;
}
// Trigger form visibility update
const event = new Event('change');
document.querySelector('input[name="networkMode"]:checked')?.dispatchEvent(event);
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Configuration not found in window.config");
}
});

View File

@ -0,0 +1,131 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function isValidCIDR(cidr) {
try {
const [ip, mask] = cidr.split('/');
if (!ip || !mask) return false;
// Validate IP address
if (ip.includes(':')) {
// IPv6
if (!/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(ip)) return false;
} else {
// IPv4
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) return false;
}
// Validate mask
const maskNum = parseInt(mask);
if (isNaN(maskNum)) return false;
if (ip.includes(':')) {
// IPv6
if (maskNum < 0 || maskNum > 128) return false;
} else {
// IPv4
if (maskNum < 0 || maskNum > 32) return false;
}
return true;
} catch {
return false;
}
}
function populateSubnetList(selectId, subnets) {
const select = document.getElementById(selectId);
select.innerHTML = ''; // Clear existing options
subnets.forEach(subnet => {
const option = document.createElement('option');
option.value = subnet;
option.text = subnet;
select.appendChild(option);
});
}
function addSubnet(listId, inputId) {
const input = document.getElementById(inputId);
const cidr = input.value.trim();
if (!isValidCIDR(cidr)) {
alert('Please enter a valid CIDR notation (e.g. 192.168.1.0/24 or 2001:db8::/32)');
return;
}
const select = document.getElementById(listId);
// Check for duplicates
for (let i = 0; i < select.options.length; i++) {
if (select.options[i].value === cidr) {
alert('This CIDR is already in the list');
return;
}
}
const option = document.createElement('option');
option.value = cidr;
option.text = cidr;
select.appendChild(option);
input.value = ''; // Clear input
}
function removeSubnet(listId) {
const select = document.getElementById(listId);
const selected = Array.from(select.selectedOptions);
selected.forEach(option => select.removeChild(option));
}
function getSubnetsFromList(listId) {
const select = document.getElementById(listId);
return Array.from(select.options).map(option => option.value);
}
function updateConfig() {
// Update only the ip_ranges section
window.config.ip_ranges = {
ignore_subnets: getSubnetsFromList('ignoredSubnets'),
allow_subnets: getSubnetsFromList('allowedSubnets'),
unknown_ip_honors_ignore: document.getElementById('unknownHonorsIgnore').checked,
unknown_ip_honors_allow: document.getElementById('unknownHonorsAllow').checked
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.ip_ranges) {
const ipRanges = window.config.ip_ranges;
// Populate subnet lists
populateSubnetList('ignoredSubnets', ipRanges.ignore_subnets);
populateSubnetList('allowedSubnets', ipRanges.allow_subnets);
// Set checkbox states
document.getElementById('unknownHonorsIgnore').checked =
ipRanges.unknown_ip_honors_ignore ?? true;
document.getElementById('unknownHonorsAllow').checked =
ipRanges.unknown_ip_honors_allow ?? true;
// Add event listeners
document.getElementById('addIgnoredSubnet').addEventListener('click', () => {
addSubnet('ignoredSubnets', 'newIgnoredSubnet');
});
document.getElementById('removeIgnoredSubnet').addEventListener('click', () => {
removeSubnet('ignoredSubnets');
});
document.getElementById('addAllowedSubnet').addEventListener('click', () => {
addSubnet('allowedSubnets', 'newAllowedSubnet');
});
document.getElementById('removeAllowedSubnet').addEventListener('click', () => {
removeSubnet('allowedSubnets');
});
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
});
} else {
console.error("IP Ranges configuration not found in window.config");
}
});

View File

@ -0,0 +1,73 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate numeric fields
const collationPeriod = parseInt(document.getElementById("collationPeriod").value);
if (isNaN(collationPeriod) || collationPeriod < 1) {
alert("Collation Period must be a number greater than 0");
return false;
}
const uispInterval = parseInt(document.getElementById("uispInterval").value);
if (isNaN(uispInterval) || uispInterval < 0) {
alert("UISP Reporting Interval must be a number of at least 0");
return false;
}
// Validate URL format if provided
const ltsUrl = document.getElementById("ltsUrl").value.trim();
if (ltsUrl) {
try {
new URL(ltsUrl);
} catch {
alert("LTS Server URL must be a valid URL");
return false;
}
}
return true;
}
function updateConfig() {
// Update only the long-term stats section
window.config.long_term_stats = {
gather_stats: document.getElementById("gatherStats").checked,
collation_period_seconds: parseInt(document.getElementById("collationPeriod").value),
license_key: document.getElementById("licenseKey").value.trim() || null,
uisp_reporting_interval_seconds: parseInt(document.getElementById("uispInterval").value) || null,
lts_url: document.getElementById("ltsUrl").value.trim() || null,
use_insight: document.getElementById("useInsight").checked
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.long_term_stats) {
const lts = window.config.long_term_stats;
// Boolean fields
document.getElementById("gatherStats").checked = lts.gather_stats ?? true;
document.getElementById("useInsight").checked = lts.use_insight ?? false;
// Numeric fields
document.getElementById("collationPeriod").value = lts.collation_period_seconds ?? 60;
document.getElementById("uispInterval").value = lts.uisp_reporting_interval_seconds ?? 300;
// Optional string fields
document.getElementById("licenseKey").value = lts.license_key ?? "";
document.getElementById("ltsUrl").value = lts.lts_url ?? "";
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Long-term stats configuration not found in window.config");
}
});

View File

@ -0,0 +1,273 @@
import {saveNetworkAndDevices} from "./config/config_helper";
let network_json = null;
let shaped_devices = null;
function renderNetworkNode(level, depth) {
let html = `<div class="card mb-3" style="margin-left: ${depth * 30}px;">`;
html += `<div class="card-body">`;
for (const [key, value] of Object.entries(level)) {
// Node header with actions
html += `<div class="d-flex justify-content-between align-items-center mb-2">`;
html += `<h5 class="card-title mb-0">${key}</h5>`;
html += `<div>`;
if (depth > 0) {
html += `<button class="btn btn-sm btn-outline-secondary me-1" onclick="promoteNode('${key}')">
<i class="fas fa-arrow-up"></i> Promote
</button>`;
}
html += `<button class="btn btn-sm btn-outline-secondary me-1" onclick="renameNode('${key}')">
<i class="fas fa-pencil-alt"></i> Rename
</button>`;
html += `<button class="btn btn-sm btn-outline-danger" onclick="deleteNode('${key}')">
<i class="fas fa-trash-alt"></i> Delete
</button>`;
html += `</div></div>`;
// Node details
html += `<div class="mb-3">`;
html += `<span class="badge bg-primary me-2">Download: ${value.downloadBandwidthMbps} Mbps</span>`;
html += `<button class="btn btn-sm btn-outline-secondary me-2" onclick="nodeSpeedChange('${key}', 'd')">
<i class="fas fa-pencil-alt"></i>
</button>`;
html += `<span class="badge bg-success me-2">Upload: ${value.uploadBandwidthMbps} Mbps</span>`;
html += `<button class="btn btn-sm btn-outline-secondary" onclick="nodeSpeedChange('${key}', 'u')">
<i class="fas fa-pencil-alt"></i>
</button>`;
html += `</div>`;
// Child nodes
if (value.children) {
html += renderNetworkNode(value.children, depth + 1);
}
}
html += `</div></div>`;
return html;
}
function renderNetwork() {
if (!network_json || Object.keys(network_json).length === 0) {
$("#netjson").html(`<div class="alert alert-info">No network nodes found. Add one to get started!</div>`);
return;
}
$("#netjson").html(renderNetworkNode(network_json, 0));
}
function promoteNode(nodeId) {
console.log("Promoting ", nodeId);
let previousParent = null;
function iterate(tree, depth) {
for (const [key, value] of Object.entries(tree)) {
if (key === nodeId) {
let tmp = value;
delete tree[nodeId];
previousParent[nodeId] = tmp;
}
if (value.children != null) {
previousParent = tree;
iterate(value.children, depth+1);
}
}
}
iterate(network_json);
renderNetwork();
}
function nodeSpeedChange(nodeId, direction) {
let newVal = prompt(`New ${direction === 'd' ? 'download' : 'upload'} value in Mbps`);
newVal = parseInt(newVal);
if (isNaN(newVal)) {
alert("Please enter a valid number");
return;
}
if (newVal < 1) {
alert("Value must be greater than 1");
return;
}
function iterate(tree) {
for (const [key, value] of Object.entries(tree)) {
if (key === nodeId) {
if (direction === 'd') {
value.downloadBandwidthMbps = newVal;
} else {
value.uploadBandwidthMbps = newVal;
}
}
if (value.children != null) {
iterate(value.children);
}
}
}
iterate(network_json);
renderNetwork();
}
function deleteNode(nodeId) {
if (!confirm(`Are you sure you want to delete ${nodeId} and all its children?`)) {
return;
}
let deleteList = [ nodeId ];
let deleteParent = "";
// Find the node to delete
function iterate(tree, depth, parent) {
for (const [key, value] of Object.entries(tree)) {
if (key === nodeId) {
// Find nodes that will go away
if (value.children != null) {
iterateTargets(value.children, depth+1);
}
deleteParent = parent;
delete tree[key];
}
if (value.children != null) {
iterate(value.children, depth+1, key);
}
}
}
function iterateTargets(tree, depth) {
for (const [key, value] of Object.entries(tree)) {
deleteList.push(key);
if (value.children != null) {
iterateTargets(value.children, depth+1);
}
}
}
// Find the nodes to delete and erase them
iterate(network_json, "");
// Now we have a list in deleteList of all the nodes that were deleted
// We need to go through ShapedDevices and re-parent devices
console.log(deleteParent);
if (deleteParent == null) {
// We deleted something at the top of the tree, so there's no
// natural parent! So we'll set them to be at the root. That's
// only really the right answer if the user went "flat" - but there's
// no way to know. So they'll have to fix some validation themselves.
for (let i=0; i<shaped_devices.length; i++) {
let sd = shaped_devices[i];
if (deleteList.indexOf(sd.parent_node) > -1) {
sd.parent_node = "";
}
}
alert("Because there was no obvious parent, you may have to fix some parenting in your Shaped Devices list.");
} else {
// Move everything up the tree
for (let i=0; i<shaped_devices.length; i++) {
let sd = shaped_devices[i];
if (deleteList.indexOf(sd.parent_node) > -1) {
sd.parent_node = deleteParent;
}
}
}
// Update the display
renderNetwork();
shapedDevices();
}
function renameNode(nodeId) {
let newName = prompt("New node name?");
if (!newName || newName.trim() === "") {
alert("Please enter a valid name");
return;
}
// Check if the new name already exists
function checkExists(tree) {
for (const [key, _] of Object.entries(tree)) {
if (key === newName) {
return true;
}
if (tree[key].children) {
if (checkExists(tree[key].children)) {
return true;
}
}
}
return false;
}
if (checkExists(network_json)) {
alert("A node with that name already exists");
return;
}
function iterate(tree, depth) {
for (const [key, value] of Object.entries(tree)) {
if (key === nodeId) {
let tmp = value;
delete tree[nodeId];
tree[newName] = tmp;
}
if (value.children != null) {
iterate(value.children, depth+1);
}
}
}
iterate(network_json);
// Update shaped devices
for (let i=0; i<shaped_devices.length; i++) {
let sd = shaped_devices[i];
if (sd.parent_node === nodeId) {
sd.parent_node = newName;
}
}
renderNetwork();
shapedDevices();
}
function start() {
// Add links
window.promoteNode = promoteNode;
window.renameNode = renameNode;
window.deleteNode = deleteNode;
window.nodeSpeedChange = nodeSpeedChange;
// Add save button handler
// Add network save button handler
$("#btnSaveNetwork").on('click', () => {
// Validate network structure
if (!network_json || Object.keys(network_json).length === 0) {
alert("Network configuration is empty");
return;
}
// Save with empty shaped_devices since we're only saving network
saveNetworkAndDevices(network_json, shaped_devices, (success, message) => {
if (success) {
alert(message);
} else {
alert("Failed to save network configuration: " + message);
}
});
});
// Load network data
$.get("/local-api/allShapedDevices", (data) => {
shaped_devices = data;
$.get("/local-api/networkJson", (njs) => {
network_json = njs;
renderNetwork();
});
});
}
$(document).ready(start);

View File

@ -0,0 +1,64 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate required fields when enabled
if (document.getElementById("enablePowercode").checked) {
const apiKey = document.getElementById("powercodeApiKey").value.trim();
if (!apiKey) {
alert("API Key is required when Powercode integration is enabled");
return false;
}
const apiUrl = document.getElementById("powercodeApiUrl").value.trim();
if (!apiUrl) {
alert("API URL is required when Powercode integration is enabled");
return false;
}
try {
new URL(apiUrl);
} catch {
alert("API URL must be a valid URL");
return false;
}
}
return true;
}
function updateConfig() {
// Update only the powercode_integration section
window.config.powercode_integration = {
enable_powercode: document.getElementById("enablePowercode").checked,
powercode_api_key: document.getElementById("powercodeApiKey").value.trim(),
powercode_api_url: document.getElementById("powercodeApiUrl").value.trim()
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.powercode_integration) {
const powercode = window.config.powercode_integration;
// Boolean field
document.getElementById("enablePowercode").checked =
powercode.enable_powercode ?? false;
// String fields
document.getElementById("powercodeApiKey").value =
powercode.powercode_api_key ?? "";
document.getElementById("powercodeApiUrl").value =
powercode.powercode_api_url ?? "";
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Powercode integration configuration not found in window.config");
}
});

View File

@ -0,0 +1,91 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate numeric fields
const uplink = parseInt(document.getElementById("uplinkBandwidth").value);
if (isNaN(uplink) || uplink < 1) {
alert("Uplink Bandwidth must be a number greater than 0");
return false;
}
const downlink = parseInt(document.getElementById("downlinkBandwidth").value);
if (isNaN(downlink) || downlink < 1) {
alert("Downlink Bandwidth must be a number greater than 0");
return false;
}
const pnDownload = parseInt(document.getElementById("generatedPnDownload").value);
if (isNaN(pnDownload) || pnDownload < 1) {
alert("Per-Node Download must be a number greater than 0");
return false;
}
const pnUpload = parseInt(document.getElementById("generatedPnUpload").value);
if (isNaN(pnUpload) || pnUpload < 1) {
alert("Per-Node Upload must be a number greater than 0");
return false;
}
const overrideQueues = document.getElementById("overrideQueues").value;
if (overrideQueues && (isNaN(overrideQueues) || overrideQueues < 1)) {
alert("Override Queues must be a number greater than 0");
return false;
}
return true;
}
function updateConfig() {
// Update only the queues section
window.config.queues = {
default_sqm: document.getElementById("defaultSqm").value,
monitor_only: document.getElementById("monitorOnly").checked,
uplink_bandwidth_mbps: parseInt(document.getElementById("uplinkBandwidth").value),
downlink_bandwidth_mbps: parseInt(document.getElementById("downlinkBandwidth").value),
generated_pn_download_mbps: parseInt(document.getElementById("generatedPnDownload").value),
generated_pn_upload_mbps: parseInt(document.getElementById("generatedPnUpload").value),
dry_run: document.getElementById("dryRun").checked,
sudo: document.getElementById("sudo").checked,
override_available_queues: document.getElementById("overrideQueues").value ?
parseInt(document.getElementById("overrideQueues").value) : null,
use_binpacking: document.getElementById("useBinpacking").checked
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.queues) {
const queues = window.config.queues;
// Select field
document.getElementById("defaultSqm").value = queues.default_sqm;
// Boolean fields
document.getElementById("monitorOnly").checked = queues.monitor_only ?? false;
document.getElementById("dryRun").checked = queues.dry_run ?? false;
document.getElementById("sudo").checked = queues.sudo ?? false;
document.getElementById("useBinpacking").checked = queues.use_binpacking ?? false;
// Numeric fields
document.getElementById("uplinkBandwidth").value = queues.uplink_bandwidth_mbps ?? 1000;
document.getElementById("downlinkBandwidth").value = queues.downlink_bandwidth_mbps ?? 1000;
document.getElementById("generatedPnDownload").value = queues.generated_pn_download_mbps ?? 1000;
document.getElementById("generatedPnUpload").value = queues.generated_pn_upload_mbps ?? 1000;
// Optional numeric field
document.getElementById("overrideQueues").value = queues.override_available_queues ?? "";
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Queue configuration not found in window.config");
}
});

View File

@ -0,0 +1,92 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function arrayToString(arr) {
return arr ? arr.join(', ') : '';
}
function stringToArray(str) {
return str ? str.split(',').map(s => s.trim()).filter(s => s.length > 0) : [];
}
function validateConfig() {
// Validate required fields when enabled
if (document.getElementById("enableSonar").checked) {
const apiUrl = document.getElementById("sonarApiUrl").value.trim();
if (!apiUrl) {
alert("API URL is required when Sonar integration is enabled");
return false;
}
try {
new URL(apiUrl);
} catch {
alert("API URL must be a valid URL");
return false;
}
const apiKey = document.getElementById("sonarApiKey").value.trim();
if (!apiKey) {
alert("API Key is required when Sonar integration is enabled");
return false;
}
const snmpCommunity = document.getElementById("snmpCommunity").value.trim();
if (!snmpCommunity) {
alert("SNMP Community is required when Sonar integration is enabled");
return false;
}
}
return true;
}
function updateConfig() {
// Update only the sonar_integration section
window.config.sonar_integration = {
enable_sonar: document.getElementById("enableSonar").checked,
sonar_api_url: document.getElementById("sonarApiUrl").value.trim(),
sonar_api_key: document.getElementById("sonarApiKey").value.trim(),
snmp_community: document.getElementById("snmpCommunity").value.trim(),
airmax_model_ids: stringToArray(document.getElementById("airmaxModelIds").value),
ltu_model_ids: stringToArray(document.getElementById("ltuModelIds").value),
active_status_ids: stringToArray(document.getElementById("activeStatusIds").value)
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.sonar_integration) {
const sonar = window.config.sonar_integration;
// Boolean field
document.getElementById("enableSonar").checked =
sonar.enable_sonar ?? false;
// String fields
document.getElementById("sonarApiUrl").value =
sonar.sonar_api_url ?? "";
document.getElementById("sonarApiKey").value =
sonar.sonar_api_key ?? "";
document.getElementById("snmpCommunity").value =
sonar.snmp_community ?? "public";
// Array fields (convert to comma-separated strings)
document.getElementById("airmaxModelIds").value =
arrayToString(sonar.airmax_model_ids);
document.getElementById("ltuModelIds").value =
arrayToString(sonar.ltu_model_ids);
document.getElementById("activeStatusIds").value =
arrayToString(sonar.active_status_ids);
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Sonar integration configuration not found in window.config");
}
});

View File

@ -0,0 +1,73 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate required fields when enabled
if (document.getElementById("enableSplynx").checked) {
const apiKey = document.getElementById("apiKey").value.trim();
if (!apiKey) {
alert("API Key is required when Splynx integration is enabled");
return false;
}
const apiSecret = document.getElementById("apiSecret").value.trim();
if (!apiSecret) {
alert("API Secret is required when Splynx integration is enabled");
return false;
}
const url = document.getElementById("spylnxUrl").value.trim();
if (!url) {
alert("Splynx URL is required when Splynx integration is enabled");
return false;
}
try {
new URL(url);
} catch {
alert("Splynx URL must be a valid URL");
return false;
}
}
return true;
}
function updateConfig() {
// Update only the spylnx_integration section
window.config.spylnx_integration = {
enable_spylnx: document.getElementById("enableSplynx").checked,
api_key: document.getElementById("apiKey").value.trim(),
api_secret: document.getElementById("apiSecret").value.trim(),
url: document.getElementById("spylnxUrl").value.trim()
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.spylnx_integration) {
const spylnx = window.config.spylnx_integration;
// Boolean field
document.getElementById("enableSplynx").checked =
spylnx.enable_spylnx ?? false;
// String fields
document.getElementById("apiKey").value =
spylnx.api_key ?? "";
document.getElementById("apiSecret").value =
spylnx.api_secret ?? "";
document.getElementById("spylnxUrl").value =
spylnx.url ?? "";
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Splynx integration configuration not found in window.config");
}
});

View File

@ -0,0 +1,82 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate numeric fields
const netdevBudgetUsecs = parseInt(document.getElementById("netdevBudgetUsecs").value);
if (isNaN(netdevBudgetUsecs) || netdevBudgetUsecs < 0) {
alert("Netdev Budget (μs) must be a positive number");
return false;
}
const netdevBudgetPackets = parseInt(document.getElementById("netdevBudgetPackets").value);
if (isNaN(netdevBudgetPackets) || netdevBudgetPackets < 0) {
alert("Netdev Budget (Packets) must be a positive number");
return false;
}
const rxUsecs = parseInt(document.getElementById("rxUsecs").value);
if (isNaN(rxUsecs) || rxUsecs < 0) {
alert("RX Polling Frequency (μs) must be a positive number");
return false;
}
const txUsecs = parseInt(document.getElementById("txUsecs").value);
if (isNaN(txUsecs) || txUsecs < 0) {
alert("TX Polling Frequency (μs) must be a positive number");
return false;
}
return true;
}
function updateConfig() {
// Update only the tuning section
window.config.tuning = {
stop_irq_balance: document.getElementById("stopIrqBalance").checked,
netdev_budget_usecs: parseInt(document.getElementById("netdevBudgetUsecs").value),
netdev_budget_packets: parseInt(document.getElementById("netdevBudgetPackets").value),
rx_usecs: parseInt(document.getElementById("rxUsecs").value),
tx_usecs: parseInt(document.getElementById("txUsecs").value),
disable_rxvlan: document.getElementById("disableRxVlan").checked,
disable_txvlan: document.getElementById("disableTxVlan").checked,
disable_offload: document.getElementById("disableOffload").value
.split(' ')
.map(s => s.trim())
.filter(s => s.length > 0)
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.tuning) {
const tunables = window.config.tuning;
// Boolean fields
document.getElementById("stopIrqBalance").checked = tunables.stop_irq_balance ?? false;
document.getElementById("disableRxVlan").checked = tunables.disable_rxvlan ?? false;
document.getElementById("disableTxVlan").checked = tunables.disable_txvlan ?? false;
// Numeric fields
document.getElementById("netdevBudgetUsecs").value = tunables.netdev_budget_usecs ?? 8000;
document.getElementById("netdevBudgetPackets").value = tunables.netdev_budget_packets ?? 300;
document.getElementById("rxUsecs").value = tunables.rx_usecs ?? 8;
document.getElementById("txUsecs").value = tunables.tx_usecs ?? 8;
// Array field (convert to space-separated string)
document.getElementById("disableOffload").value =
(tunables.disable_offload ?? ["gso", "tso", "lro", "sg", "gro"]).join(' ');
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("Tuning configuration not found in window.config");
}
});

View File

@ -0,0 +1,130 @@
import {saveConfig, loadConfig} from "./config/config_helper";
function validateConfig() {
// Validate required fields when enabled
if (document.getElementById("enableUisp").checked) {
const token = document.getElementById("uispToken").value.trim();
if (!token) {
alert("API Token is required when UISP integration is enabled");
return false;
}
const url = document.getElementById("uispUrl").value.trim();
if (!url) {
alert("UISP URL is required when UISP integration is enabled");
return false;
}
try {
new URL(url);
} catch {
alert("UISP URL must be a valid URL");
return false;
}
const site = document.getElementById("uispSite").value.trim();
if (!site) {
alert("UISP Site is required when UISP integration is enabled");
return false;
}
const strategy = document.getElementById("uispStrategy").value.trim();
if (!strategy) {
alert("Strategy is required when UISP integration is enabled");
return false;
}
const suspendedStrategy = document.getElementById("uispSuspendedStrategy").value.trim();
if (!suspendedStrategy) {
alert("Suspended Strategy is required when UISP integration is enabled");
return false;
}
// Validate numeric fields
const airmaxCapacity = parseFloat(document.getElementById("uispAirmaxCapacity").value);
if (isNaN(airmaxCapacity) || airmaxCapacity < 0) {
alert("Airmax Capacity must be a number greater than or equal to 0");
return false;
}
const ltuCapacity = parseFloat(document.getElementById("uispLtuCapacity").value);
if (isNaN(ltuCapacity) || ltuCapacity < 0) {
alert("LTU Capacity must be a number greater than or equal to 0");
return false;
}
const bandwidthOverhead = parseFloat(document.getElementById("uispBandwidthOverhead").value);
if (isNaN(bandwidthOverhead) || bandwidthOverhead <= 0) {
alert("Bandwidth Overhead Factor must be a number greater than 0");
return false;
}
const commitMultiplier = parseFloat(document.getElementById("uispCommitMultiplier").value);
if (isNaN(commitMultiplier) || commitMultiplier <= 0) {
alert("Commit Bandwidth Multiplier must be a number greater than 0");
return false;
}
}
return true;
}
function updateConfig() {
// Update only the uisp_integration section
window.config.uisp_integration = {
enable_uisp: document.getElementById("enableUisp").checked,
token: document.getElementById("uispToken").value.trim(),
url: document.getElementById("uispUrl").value.trim(),
site: document.getElementById("uispSite").value.trim(),
strategy: document.getElementById("uispStrategy").value.trim(),
suspended_strategy: document.getElementById("uispSuspendedStrategy").value.trim(),
airmax_capacity: parseFloat(document.getElementById("uispAirmaxCapacity").value),
ltu_capacity: parseFloat(document.getElementById("uispLtuCapacity").value),
ipv6_with_mikrotik: document.getElementById("uispIpv6WithMikrotik").checked,
bandwidth_overhead_factor: parseFloat(document.getElementById("uispBandwidthOverhead").value),
commit_bandwidth_multiplier: parseFloat(document.getElementById("uispCommitMultiplier").value),
use_ptmp_as_parent: document.getElementById("uispUsePtmpAsParent").checked,
ignore_calculated_capacity: document.getElementById("uispIgnoreCalculatedCapacity").checked,
// Default values for fields not in the form
exclude_sites: [],
squash_sites: null,
exception_cpes: []
};
}
loadConfig(() => {
// window.config now contains the configuration.
// Populate form fields with config values
if (window.config && window.config.uisp_integration) {
const uisp = window.config.uisp_integration;
// Boolean fields
document.getElementById("enableUisp").checked = uisp.enable_uisp ?? false;
document.getElementById("uispIpv6WithMikrotik").checked = uisp.ipv6_with_mikrotik ?? false;
document.getElementById("uispUsePtmpAsParent").checked = uisp.use_ptmp_as_parent ?? false;
document.getElementById("uispIgnoreCalculatedCapacity").checked = uisp.ignore_calculated_capacity ?? false;
// String fields
document.getElementById("uispToken").value = uisp.token ?? "";
document.getElementById("uispUrl").value = uisp.url ?? "";
document.getElementById("uispSite").value = uisp.site ?? "";
document.getElementById("uispStrategy").value = uisp.strategy ?? "";
document.getElementById("uispSuspendedStrategy").value = uisp.suspended_strategy ?? "";
// Numeric fields
document.getElementById("uispAirmaxCapacity").value = uisp.airmax_capacity ?? 0.0;
document.getElementById("uispLtuCapacity").value = uisp.ltu_capacity ?? 0.0;
document.getElementById("uispBandwidthOverhead").value = uisp.bandwidth_overhead_factor ?? 1.0;
document.getElementById("uispCommitMultiplier").value = uisp.commit_bandwidth_multiplier ?? 1.0;
// Add save button click handler
document.getElementById('saveButton').addEventListener('click', () => {
if (validateConfig()) {
updateConfig();
saveConfig(() => {
alert("Configuration saved successfully!");
});
}
});
} else {
console.error("UISP integration configuration not found in window.config");
}
});

View File

@ -0,0 +1,128 @@
$(document).ready(() => {
loadUsers();
// Handle add user form submission
$('#add-user-form').on('submit', function(e) {
e.preventDefault();
const username = $('#add-username').val().trim();
const password = $('#password').val();
const role = $('#role').val();
console.log(username, password, role);
if (!username) {
alert('Username cannot be empty');
return;
}
$.ajax({
type: "POST",
url: "/local-api/addUser",
data: JSON.stringify({
username: username,
password: password,
role: role
}),
contentType: 'application/json',
success: () => {
$('#username').val('');
$('#password').val('');
loadUsers();
},
error: (e) => {
alert('Failed to add user');
console.log(e);
}
});
});
// Handle edit user form submission
$('#save-user-changes').on('click', function() {
const username = $('#edit-username').val();
const password = $('#edit-password').val();
const role = $('#edit-role').val();
$.ajax({
type: "POST",
url: "/local-api/updateUser",
data: JSON.stringify({
username: username,
password: password,
role: role
}),
contentType: 'application/json',
success: () => {
$('#editUserModal').modal('hide');
loadUsers();
},
error: () => {
alert('Failed to update user');
}
});
});
});
function loadUsers() {
$.get('/local-api/getUsers', (users) => {
const userList = $('#users-list');
userList.empty();
if (users.length === 0) {
userList.html('<div class="alert alert-info">No users found</div>');
return;
}
const table = $('<table class="table table-striped">')
.append('<thead><tr><th>Username</th><th>Role</th><th>Actions</th></tr></thead>');
const tbody = $('<tbody>');
users.forEach(user => {
const row = $('<tr>')
.append(`<td>${user.username}</td>`)
.append(`<td>${user.role}</td>`)
.append(`<td>
<button class="btn btn-sm btn-primary edit-user" data-username="${user.username}">
<i class="fa fa-edit"></i> Edit
</button>
<button class="btn btn-sm btn-danger delete-user" data-username="${user.username}">
<i class="fa fa-trash"></i> Delete
</button>
</td>`);
tbody.append(row);
});
table.append(tbody);
userList.append(table);
// Attach edit handlers
$('.edit-user').on('click', function() {
const username = $(this).data('username');
const user = users.find(u => u.username === username);
$('#edit-username').val(user.username);
$('#edit-role').val(user.role);
$('#editUserModal').modal('show');
});
// Attach delete handlers
$('.delete-user').on('click', function() {
if (confirm('Are you sure you want to delete this user?')) {
const username = $(this).data('username');
$.ajax({
type: "POST",
url: "/local-api/deleteUser",
data: JSON.stringify({ username: username }),
contentType: 'application/json',
success: () => {
loadUsers();
},
error: (e) => {
console.error(e);
alert('Failed to delete user');
}
});
}
});
}).fail(() => {
$('#users-list').html('<div class="alert alert-danger">Failed to load users</div>');
});
}

View File

@ -783,6 +783,82 @@ function checkIpv6Duplicate(ip, index) {
return -1;
}
function validNodeList() {
let nodes = [];
function iterate(data, level) {
for (const [key, value] of Object.entries(data)) {
nodes.push(key);
if (value.children != null)
iterate(value.children, level+1);
}
}
iterate(network_json, 0);
return nodes;
}
function checkIpv4(ip) {
const ipv4Pattern =
/^(\d{1,3}\.){3}\d{1,3}$/;
if (ip.indexOf('/') === -1) {
return ipv4Pattern.test(ip);
} else {
let parts = ip.split('/');
return ipv4Pattern.test(parts[0]);
}
}
function checkIpv6(ip) {
// Check if the input is a valid IPv6 address with prefix
const regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]{1,3}))?$/;
return regex.test(ip);
}
function checkIpv4Duplicate(ip, index) {
ip = ip.trim();
for (let i=0; i < shaped_devices.length; i++) {
if (i !== index) {
let sd = shaped_devices[i];
for (let j=0; j<sd.ipv4.length; j++) {
let formatted = "";
if (ip.indexOf('/') > 0) {
formatted = sd.ipv4[j][0] + "/" + sd.ipv4[j][1];
} else {
formatted = sd.ipv4[j][0];
}
if (formatted === ip) {
return index;
}
}
}
}
return -1;
}
function checkIpv6Duplicate(ip, index) {
ip = ip.trim();
for (let i=0; i < shaped_devices.length; i++) {
if (i !== index) {
let sd = shaped_devices[i];
for (let j=0; j<sd.ipv6.length; j++) {
let formatted = "";
if (ip.indexOf('/') > 0) {
formatted = sd.ipv6[j][0] + "/" + sd.ipv6[j][1];
} else {
formatted = sd.ipv6[j][0];
}
if (formatted === ip) {
return index;
}
}
}
}
return -1;
}
function validateSd() {
let valid = true;
let errors = [];
@ -1183,7 +1259,13 @@ function start() {
saveConfig();
});
$("#btnSaveNetDevices").on('click', (data) => {
saveNetAndDevices();
saveNetworkAndDevices(network_json, shaped_devices, (success, message) => {
if (success) {
alert("Network configuration saved successfully!");
} else {
alert("Failed to save network configuration: " + message);
}
});
});
}
$.get("/local-api/getConfig", (data) => {

View File

@ -47,6 +47,10 @@ pub fn local_api(shaper_query: tokio::sync::mpsc::Sender<ShaperQueryCommand>) ->
.route("/allShapedDevices", get(config::all_shaped_devices))
.route("/updateConfig", post(config::update_lqosd_config))
.route("/updateNetworkAndDevices", post(config::update_network_and_devices))
.route("/getUsers", get(config::get_users))
.route("/addUser", post(config::add_user))
.route("/updateUser", post(config::update_user))
.route("/deleteUser", post(config::delete_user))
.route("/circuitById", post(circuit::get_circuit_by_id))
.route("/requestAnalysis/:ip", get(packet_analysis::request_analysis))
.route("/pcapDump/:id", get(packet_analysis::pcap_dump))

View File

@ -1,10 +1,10 @@
use std::sync::Arc;
use axum::{Extension, Json};
use axum::http::StatusCode;
use lqos_config::{Config, ConfigShapedDevices, ShapedDevice};
use lqos_config::{Config, ConfigShapedDevices, ShapedDevice, WebUser, WebUsers};
use crate::node_manager::auth::LoginResult;
use default_net::get_interfaces;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use lqos_bus::{bus_request, BusRequest};
use crate::shaped_devices_tracker::SHAPED_DEVICES;
@ -118,3 +118,71 @@ pub async fn update_network_and_devices(
"Ok".to_string()
}
#[derive(Serialize, Deserialize)]
pub struct UserRequest {
pub username: String,
pub password: String,
pub role: String,
}
pub async fn get_users(
Extension(login): Extension<LoginResult>,
) -> Result<Json<Vec<WebUser>>, StatusCode> {
if login != LoginResult::Admin {
return Err(StatusCode::FORBIDDEN);
}
let users = WebUsers::load_or_create()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(users.get_users()))
}
pub async fn add_user(
Extension(login): Extension<LoginResult>,
Json(data): Json<UserRequest>,
) -> Result<String, StatusCode> {
if login != LoginResult::Admin {
return Err(StatusCode::FORBIDDEN);
}
if data.username.is_empty() {
return Err(StatusCode::BAD_REQUEST);
}
let mut users = WebUsers::load_or_create()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
users.add_or_update_user(&data.username.trim(), &data.password, data.role.into())
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(format!("User '{}' added", data.username))
}
pub async fn update_user(
Extension(login): Extension<LoginResult>,
Json(data): Json<UserRequest>,
) -> Result<String, StatusCode> {
if login != LoginResult::Admin {
return Err(StatusCode::FORBIDDEN);
}
let mut users = WebUsers::load_or_create()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
users.add_or_update_user(&data.username, &data.password, data.role.into())
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok("User updated".to_string())
}
#[derive(Deserialize)]
pub struct DeleteUserRequest {
pub username: String,
}
pub async fn delete_user(
Extension(login): Extension<LoginResult>,
Json(data): Json<DeleteUserRequest>,
) -> Result<String, StatusCode> {
if login != LoginResult::Admin {
return Err(StatusCode::FORBIDDEN);
}
let mut users = WebUsers::load_or_create()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
users.remove_user(&data.username)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok("User deleted".to_string())
}

View File

@ -1,6 +1,8 @@
use std::time::Duration;
use itertools::Itertools;
use serde::Serialize;
use tracing::warn;
use lqos_config::load_config;
use lqos_utils::units::DownUpOrder;
use lqos_utils::unix_time::time_since_boot;
use crate::shaped_devices_tracker::SHAPED_DEVICES;
@ -17,6 +19,13 @@ pub struct UnknownIp {
pub fn get_unknown_ips() -> Vec<UnknownIp> {
const FIVE_MINUTES_IN_NANOS: u64 = 5 * 60 * 1_000_000_000;
let Ok(config) = load_config() else {
warn!("Failed to load config");
return vec![];
};
let allowed_ips = config.ip_ranges.allowed_network_table();
let ignored_ips = config.ip_ranges.ignored_network_table();
let now = Duration::from(time_since_boot().unwrap()).as_nanos() as u64;
let sd_reader = SHAPED_DEVICES.load();
THROUGHPUT_TRACKER
@ -24,12 +33,25 @@ pub fn get_unknown_ips() -> Vec<UnknownIp> {
.lock()
.unwrap()
.iter()
// Remove all loopback devices
.filter(|(k,_v)| !k.as_ip().is_loopback())
// Remove any items that have a tc_handle of 0
.filter(|(_k,d)| d.tc_handle.as_u32() == 0)
// Remove any items that are matched by the shaped devices file
.filter(|(k,_d)| {
let ip = k.as_ip();
!sd_reader.trie.longest_match(ip).is_some()
// If the IP is in the ignored list, ignore it
if config.ip_ranges.unknown_ip_honors_ignore.unwrap_or(true) && ignored_ips.longest_match(ip).is_some() {
return false;
}
// If the IP is not in the allowed list, ignore it
if config.ip_ranges.unknown_ip_honors_allow.unwrap_or(true) && allowed_ips.longest_match(ip).is_none() {
return false;
}
// If the IP is in shaped devices, ignore it
sd_reader.trie.longest_match(ip).is_none()
})
// Convert to UnknownIp
.map(|(k,d)| {
UnknownIp {
ip: k.as_ip().to_string(),
@ -38,6 +60,7 @@ pub fn get_unknown_ips() -> Vec<UnknownIp> {
current_bytes: d.bytes_per_second,
}
})
// Remove any items that have not been seen in the last 5 minutes
.filter(|u| u.last_seen_nanos <FIVE_MINUTES_IN_NANOS )
.sorted_by(|a, b| a.last_seen_nanos.cmp(&b.last_seen_nanos))
.collect()

View File

@ -0,0 +1,44 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item active"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="sendAnonymous">
<label class="form-check-label" for="sendAnonymous">Send Anonymous Usage Statistics</label>
<div class="form-text">Help improve LibreQoS by sharing anonymous usage data</div>
</div>
<div class="mb-3">
<label for="anonymousServer" class="form-label">Statistics Server</label>
<input type="text" class="form-control" id="anonymousServer" aria-describedby="serverHelp">
<div id="serverHelp" class="form-text">Server address and port for anonymous statistics collection</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_anon.js"></script>

View File

@ -0,0 +1,35 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item active"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
<div class="alert alert-warning" role="alert">
<i class="fa fa-exclamation-triangle"></i> If you have an integration active, these values will be automatically generated and any edits you perform here will be lost on the next update.
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<button id="btnSaveDevices" class="btn btn-outline-primary mb-3">
<i class="fa fa-save"></i> Save Changes
</button>
<div id="shapedDeviceTable"></div>
<script src="config_devices.js"></script>
</div>
</div>

View File

@ -0,0 +1,79 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item active"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3">
<label for="flowTimeout" class="form-label">Flow Timeout (seconds)</label>
<input type="number" class="form-control" id="flowTimeout" min="1">
<div class="form-text">How long to keep flow records before expiring them</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enableNetflow">
<label class="form-check-label" for="enableNetflow">Enable Netflow</label>
<div class="form-text">Enable Netflow export of flow data</div>
</div>
<div class="mb-3">
<label for="netflowPort" class="form-label">Netflow Port</label>
<input type="number" class="form-control" id="netflowPort" min="1" max="65535">
<div class="form-text">Port to send Netflow data to (optional)</div>
</div>
<div class="mb-3">
<label for="netflowIP" class="form-label">Netflow IP Address</label>
<input type="text" class="form-control" id="netflowIP">
<div class="form-text">IP address to send Netflow data to (optional)</div>
</div>
<div class="mb-3">
<label for="netflowVersion" class="form-label">Netflow Version</label>
<select class="form-select" id="netflowVersion">
<option value="5">Version 5</option>
<option value="9">Version 9</option>
</select>
<div class="form-text">Netflow protocol version to use</div>
</div>
<div class="card mb-3">
<div class="card-header">Do Not Track Subnets</div>
<div class="card-body">
<div class="mb-3">
<select class="form-select" id="doNotTrackSubnets" size="5">
</select>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" id="newDoNotTrackSubnet" placeholder="Enter IP/CIDR (e.g. 192.168.1.0/24)">
<button class="btn btn-outline-secondary" type="button" id="addDoNotTrackSubnet">Add</button>
</div>
<button class="btn btn-outline-danger" type="button" id="removeDoNotTrackSubnet">Remove Selected</button>
</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_flows.js"></script>

View File

@ -0,0 +1,65 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item active"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3">
<label for="nodeId" class="form-label">Node ID</label>
<input type="text" class="form-control" id="nodeId" aria-describedby="nodeIdHelp" disabled>
<div id="nodeIdHelp" class="form-text">Unique identifier for this node</div>
</div>
<div class="mb-3">
<label for="nodeName" class="form-label">Node Name</label>
<input type="text" class="form-control" id="nodeName">
</div>
<div class="mb-3">
<label for="packetCaptureTime" class="form-label">Packet Capture Time (seconds)</label>
<input type="number" class="form-control" id="packetCaptureTime" min="1">
</div>
<div class="mb-3">
<label for="queueCheckPeriod" class="form-label">Queue Check Period (milliseconds)</label>
<input type="number" class="form-control" id="queueCheckPeriod" min="100">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="disableWebserver">
<label class="form-check-label" for="disableWebserver">Disable Web Server</label>
<div class="form-text">Check to disable the web interface (headless mode)</div>
</div>
<div class="mb-3">
<label for="webserverListen" class="form-label">Web Server Listen Address</label>
<input type="text" class="form-control" id="webserverListen" placeholder="e.g. 0.0.0.0:80">
<div class="form-text">Leave blank for default (0.0.0.0:9123)</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_general.js"></script>

View File

@ -0,0 +1,49 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item active"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="circuitNameAsAddress">
<label class="form-check-label" for="circuitNameAsAddress">Use Circuit Name as Address</label>
<div class="form-text">Replace IP addresses with circuit names in network.json</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="alwaysOverwriteNetworkJson">
<label class="form-check-label" for="alwaysOverwriteNetworkJson">Always Overwrite network.json</label>
<div class="form-text">Force overwrite of network.json on every update</div>
</div>
<div class="mb-3">
<label for="queueRefreshInterval" class="form-label">Queue Refresh Interval (minutes)</label>
<input type="number" class="form-control" id="queueRefreshInterval" min="1">
<div class="form-text">How frequently to refresh queue data from the integration</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_integration.js"></script>

View File

@ -0,0 +1,125 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item active"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><i class="fa fa-map"></i> Network Layout</li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="networkMode" id="bridgeMode" value="bridge">
<label class="form-check-label" for="bridgeMode">
Bridge Mode
</label>
<div class="form-text">Two physical interfaces - one facing the Internet, one facing the LAN</div>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="networkMode" id="singleInterfaceMode" value="single">
<label class="form-check-label" for="singleInterfaceMode">
Single Interface Mode
</label>
<div class="form-text">Single physical interface using VLANs (on-a-stick)</div>
</div>
</div>
<!-- Bridge Mode Configuration -->
<div id="bridgeConfig" class="mb-3" style="display: none;">
<div class="card">
<div class="card-header">Bridge Mode Configuration</div>
<div class="card-body">
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="useXdpBridge">
<label class="form-check-label" for="useXdpBridge">Use XDP Bridge</label>
<div class="form-text">Enable XDP acceleration for the bridge</div>
</div>
<div class="mb-3">
<label for="toInternet" class="form-label">Internet-Facing Interface</label>
<input type="text" class="form-control" id="toInternet">
<div class="form-text">Interface name facing the Internet (e.g. eth0)</div>
</div>
<div class="mb-3">
<label for="toNetwork" class="form-label">LAN-Facing Interface</label>
<input type="text" class="form-control" id="toNetwork">
<div class="form-text">Interface name facing the LAN (e.g. eth1)</div>
</div>
</div>
</div>
</div>
<!-- Single Interface Mode Configuration -->
<div id="singleInterfaceConfig" class="mb-3" style="display: none;">
<div class="card">
<div class="card-header">Single Interface Mode Configuration</div>
<div class="card-body">
<div class="mb-3">
<label for="interface" class="form-label">Interface Name</label>
<input type="text" class="form-control" id="interface">
<div class="form-text">Physical interface name (e.g. eth0)</div>
</div>
<div class="mb-3">
<label for="internetVlan" class="form-label">Internet VLAN ID</label>
<input type="number" class="form-control" id="internetVlan" min="1" max="4094">
<div class="form-text">VLAN ID for Internet traffic</div>
</div>
<div class="mb-3">
<label for="networkVlan" class="form-label">LAN VLAN ID</label>
<input type="number" class="form-control" id="networkVlan" min="1" max="4094">
<div class="form-text">VLAN ID for LAN traffic</div>
</div>
</div>
</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_interface.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const bridgeMode = document.getElementById('bridgeMode');
const singleMode = document.getElementById('singleInterfaceMode');
const bridgeConfig = document.getElementById('bridgeConfig');
const singleConfig = document.getElementById('singleInterfaceConfig');
function updateFormVisibility() {
if (bridgeMode.checked) {
bridgeConfig.style.display = 'block';
singleConfig.style.display = 'none';
} else if (singleMode.checked) {
bridgeConfig.style.display = 'none';
singleConfig.style.display = 'block';
} else {
bridgeConfig.style.display = 'none';
singleConfig.style.display = 'none';
}
}
bridgeMode.addEventListener('change', updateFormVisibility);
singleMode.addEventListener('change', updateFormVisibility);
});
</script>

View File

@ -0,0 +1,89 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item active"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<div class="card-header">Ignored Subnets</div>
<div class="card-body">
<div class="mb-3">
<select class="form-select" id="ignoredSubnets" size="5">
</select>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" id="newIgnoredSubnet" placeholder="Enter IP/CIDR (e.g. 192.168.1.0/24 or 2001:db8::/32)">
<button class="btn btn-outline-secondary" type="button" id="addIgnoredSubnet">Add</button>
</div>
<button class="btn btn-outline-danger" type="button" id="removeIgnoredSubnet">Remove Selected</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Allowed Subnets</div>
<div class="card-body">
<div class="mb-3">
<select class="form-select" id="allowedSubnets" size="5">
</select>
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" id="newAllowedSubnet" placeholder="Enter IP/CIDR (e.g. 192.168.1.0/24 or 2001:db8::/32)">
<button class="btn btn-outline-secondary" type="button" id="addAllowedSubnet">Add</button>
</div>
<button class="btn btn-outline-danger" type="button" id="removeAllowedSubnet">Remove Selected</button>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="unknownHonorsIgnore">
<label class="form-check-label" for="unknownHonorsIgnore">
Unknown IPs Honor Ignore List
</label>
<div class="form-text">Should IPs not in any list be treated as ignored?</div>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="unknownHonorsAllow">
<label class="form-check-label" for="unknownHonorsAllow">
Unknown IPs Honor Allow List
</label>
<div class="form-text">Should IPs not in any list be treated as allowed?</div>
</div>
</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_iprange.js"></script>

View File

@ -0,0 +1,68 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item active"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="gatherStats">
<label class="form-check-label" for="gatherStats">Gather Long-Term Statistics</label>
<div class="form-text">Enable collection of long-term performance data</div>
</div>
<div class="mb-3">
<label for="collationPeriod" class="form-label">Collation Period (seconds)</label>
<input type="number" class="form-control" id="collationPeriod" min="1">
<div class="form-text">How frequently to aggregate statistics into long-term records</div>
</div>
<div class="mb-3">
<label for="licenseKey" class="form-label">License Key</label>
<input type="text" class="form-control" id="licenseKey">
<div class="form-text">License key for LibreQoS hosted statistics (if applicable)</div>
</div>
<div class="mb-3">
<label for="uispInterval" class="form-label">UISP Reporting Interval (seconds)</label>
<input type="number" class="form-control" id="uispInterval" min="1">
<div class="form-text">How frequently to query UISP for updates (set 0 to disable)</div>
</div>
<div class="mb-3">
<label for="ltsUrl" class="form-label">Custom LTS Server URL</label>
<input type="text" class="form-control" id="ltsUrl">
<div class="form-text">URL for self-hosted LTS server (leave blank for default)</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="useInsight">
<label class="form-check-label" for="useInsight">Enable Insight (LTS2)</label>
<div class="form-text">Experimental next-gen statistics system (alpha)</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_lts.js"></script>

View File

@ -0,0 +1,36 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item active"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
<div class="alert alert-warning" role="alert">
<i class="fa fa-exclamation-triangle"></i> If you have an integration active, these values will be automatically generated and any edits you perform here will be lost on the next update.
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<button id="btnSaveNetwork" class="btn btn-outline-primary mb-3">
<i class="fa fa-save"></i> Save Changes
</button>
<div id="netjson"></div>
</div>
</div>
<script src="config_network.js"></script>

View File

@ -0,0 +1,50 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item active"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enablePowercode">
<label class="form-check-label" for="enablePowercode">Enable Powercode Integration</label>
<div class="form-text">Enable integration with Powercode billing system</div>
</div>
<div class="mb-3">
<label for="powercodeApiKey" class="form-label">API Key</label>
<input type="text" class="form-control" id="powercodeApiKey">
<div class="form-text">Powercode API key for authentication</div>
</div>
<div class="mb-3">
<label for="powercodeApiUrl" class="form-label">API URL</label>
<input type="text" class="form-control" id="powercodeApiUrl">
<div class="form-text">Base URL for Powercode API (e.g. https://your-powercode.com)</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_powercode.js"></script>

View File

@ -0,0 +1,96 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item active"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3">
<label for="defaultSqm" class="form-label">Default SQM Algorithm</label>
<select class="form-select" id="defaultSqm">
<option value="cake diffserv4">cake diffserv4</option>
<option value="cake diffserv4 ack-filter">cake diffserv4 ack-filter</option>
<option value="fq_codel">fq_codel</option>
</select>
<div class="form-text">Queue management algorithm to use by default</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="monitorOnly">
<label class="form-check-label" for="monitorOnly">Monitor Only</label>
<div class="form-text">Monitor traffic without shaping</div>
</div>
<div class="mb-3">
<label for="uplinkBandwidth" class="form-label">Uplink Bandwidth (Mbps)</label>
<input type="number" class="form-control" id="uplinkBandwidth" min="1">
<div class="form-text">Total upstream bandwidth capacity</div>
</div>
<div class="mb-3">
<label for="downlinkBandwidth" class="form-label">Downlink Bandwidth (Mbps)</label>
<input type="number" class="form-control" id="downlinkBandwidth" min="1">
<div class="form-text">Total downstream bandwidth capacity</div>
</div>
<div class="mb-3">
<label for="generatedPnDownload" class="form-label">Generated Per-Node Download (Mbps)</label>
<input type="number" class="form-control" id="generatedPnDownload" min="1">
<div class="form-text">Download bandwidth per interface queue</div>
</div>
<div class="mb-3">
<label for="generatedPnUpload" class="form-label">Generated Per-Node Upload (Mbps)</label>
<input type="number" class="form-control" id="generatedPnUpload" min="1">
<div class="form-text">Upload bandwidth per interface queue</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="dryRun">
<label class="form-check-label" for="dryRun">Dry Run</label>
<div class="form-text">Print commands without executing them</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="sudo">
<label class="form-check-label" for="sudo">Use Sudo</label>
<div class="form-text">Prefix commands with sudo</div>
</div>
<div class="mb-3">
<label for="overrideQueues" class="form-label">Override Available Queues</label>
<input type="number" class="form-control" id="overrideQueues" min="1">
<div class="form-text">Override the number of available queues (leave blank for auto)</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="useBinpacking">
<label class="form-check-label" for="useBinpacking">Use Binpacking</label>
<div class="form-text">Use binpacking algorithm to optimize flat networks</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_queues.js"></script>

View File

@ -0,0 +1,74 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item active"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enableSonar">
<label class="form-check-label" for="enableSonar">Enable Sonar Integration</label>
<div class="form-text">Enable integration with Sonar billing system</div>
</div>
<div class="mb-3">
<label for="sonarApiUrl" class="form-label">API URL</label>
<input type="text" class="form-control" id="sonarApiUrl">
<div class="form-text">Base URL for Sonar API (e.g. https://your-sonar.com)</div>
</div>
<div class="mb-3">
<label for="sonarApiKey" class="form-label">API Key</label>
<input type="text" class="form-control" id="sonarApiKey">
<div class="form-text">Sonar API key for authentication</div>
</div>
<div class="mb-3">
<label for="snmpCommunity" class="form-label">SNMP Community</label>
<input type="text" class="form-control" id="snmpCommunity" value="public">
<div class="form-text">SNMP community string for device polling</div>
</div>
<div class="mb-3">
<label for="airmaxModelIds" class="form-label">Airmax Model IDs</label>
<input type="text" class="form-control" id="airmaxModelIds">
<div class="form-text">Comma-separated list of Airmax model IDs</div>
</div>
<div class="mb-3">
<label for="ltuModelIds" class="form-label">LTU Model IDs</label>
<input type="text" class="form-control" id="ltuModelIds">
<div class="form-text">Comma-separated list of LTU model IDs</div>
</div>
<div class="mb-3">
<label for="activeStatusIds" class="form-label">Active Status IDs</label>
<input type="text" class="form-control" id="activeStatusIds">
<div class="form-text">Comma-separated list of active status IDs</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_sonar.js"></script>

View File

@ -0,0 +1,55 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item active"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enableSplynx">
<label class="form-check-label" for="enableSplynx">Enable Splynx Integration</label>
<div class="form-text">Enable integration with Splynx billing system</div>
</div>
<div class="mb-3">
<label for="apiKey" class="form-label">API Key</label>
<input type="text" class="form-control" id="apiKey">
<div class="form-text">Splynx API key for authentication</div>
</div>
<div class="mb-3">
<label for="apiSecret" class="form-label">API Secret</label>
<input type="password" class="form-control" id="apiSecret">
<div class="form-text">Splynx API secret for authentication</div>
</div>
<div class="mb-3">
<label for="spylnxUrl" class="form-label">Splynx URL</label>
<input type="text" class="form-control" id="spylnxUrl">
<div class="form-text">Base URL for Splynx API (e.g. https://your-splynx.com)</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_spylnx.js"></script>

View File

@ -0,0 +1,81 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item active"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<div class="alert alert-warning" role="alert">
<i class="fa fa-exclamation-triangle"></i> Warning: These are advanced settings. Adjust them at your own risk.
</div>
<form>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="stopIrqBalance">
<label class="form-check-label" for="stopIrqBalance">Stop IRQ Balance Service</label>
<div class="form-text">Disables the irq_balance system service</div>
</div>
<div class="mb-3">
<label for="netdevBudgetUsecs" class="form-label">Netdev Budget (μs)</label>
<input type="number" class="form-control" id="netdevBudgetUsecs">
<div class="form-text">Time budget for network device processing in microseconds</div>
</div>
<div class="mb-3">
<label for="netdevBudgetPackets" class="form-label">Netdev Budget (Packets)</label>
<input type="number" class="form-control" id="netdevBudgetPackets">
<div class="form-text">Packet budget for network device processing</div>
</div>
<div class="mb-3">
<label for="rxUsecs" class="form-label">RX Polling Frequency (μs)</label>
<input type="number" class="form-control" id="rxUsecs">
<div class="form-text">Receive side polling frequency in microseconds</div>
</div>
<div class="mb-3">
<label for="txUsecs" class="form-label">TX Polling Frequency (μs)</label>
<input type="number" class="form-control" id="txUsecs">
<div class="form-text">Transmit side polling frequency in microseconds</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="disableRxVlan">
<label class="form-check-label" for="disableRxVlan">Disable RX VLAN Offloading</label>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="disableTxVlan">
<label class="form-check-label" for="disableTxVlan">Disable TX VLAN Offloading</label>
</div>
<div class="mb-3">
<label for="disableOffload" class="form-label">Disable Offload Features</label>
<input type="text" class="form-control" id="disableOffload" aria-describedby="offloadHelp">
<div id="offloadHelp" class="form-text">Comma-separated list of offload features to disable (e.g. gso,tso,lro)</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_tuning.js"></script>

View File

@ -0,0 +1,110 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item active"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<form>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enableUisp">
<label class="form-check-label" for="enableUisp">Enable UISP Integration</label>
<div class="form-text">Enable integration with UISP billing system</div>
</div>
<div class="mb-3">
<label for="uispToken" class="form-label">API Token</label>
<input type="text" class="form-control" id="uispToken">
<div class="form-text">UISP API token for authentication</div>
</div>
<div class="mb-3">
<label for="uispUrl" class="form-label">UISP URL</label>
<input type="text" class="form-control" id="uispUrl">
<div class="form-text">Base URL for UISP API (e.g. https://your-uisp.com)</div>
</div>
<div class="mb-3">
<label for="uispSite" class="form-label">UISP Site</label>
<input type="text" class="form-control" id="uispSite">
<div class="form-text">Site identifier in UISP</div>
</div>
<div class="mb-3">
<label for="uispStrategy" class="form-label">Strategy</label>
<input type="text" class="form-control" id="uispStrategy">
<div class="form-text">Strategy for handling devices</div>
</div>
<div class="mb-3">
<label for="uispSuspendedStrategy" class="form-label">Suspended Strategy</label>
<input type="text" class="form-control" id="uispSuspendedStrategy">
<div class="form-text">Strategy for handling suspended devices</div>
</div>
<div class="mb-3">
<label for="uispAirmaxCapacity" class="form-label">Airmax Capacity</label>
<input type="number" class="form-control" id="uispAirmaxCapacity" step="0.1">
<div class="form-text">Capacity factor for Airmax devices</div>
</div>
<div class="mb-3">
<label for="uispLtuCapacity" class="form-label">LTU Capacity</label>
<input type="number" class="form-control" id="uispLtuCapacity" step="0.1">
<div class="form-text">Capacity factor for LTU devices</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="uispIpv6WithMikrotik">
<label class="form-check-label" for="uispIpv6WithMikrotik">IPv6 with Mikrotik</label>
<div class="form-text">Enable IPv6 support with Mikrotik devices</div>
</div>
<div class="mb-3">
<label for="uispBandwidthOverhead" class="form-label">Bandwidth Overhead Factor</label>
<input type="number" class="form-control" id="uispBandwidthOverhead" step="0.1" value="1.0">
<div class="form-text">Multiplier for bandwidth overhead calculations</div>
</div>
<div class="mb-3">
<label for="uispCommitMultiplier" class="form-label">Commit Bandwidth Multiplier</label>
<input type="number" class="form-control" id="uispCommitMultiplier" step="0.1" value="1.0">
<div class="form-text">Multiplier for commit bandwidth calculations</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="uispUsePtmpAsParent">
<label class="form-check-label" for="uispUsePtmpAsParent">Use PTMP as Parent</label>
<div class="form-text">Treat PTMP devices as parent nodes</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="uispIgnoreCalculatedCapacity">
<label class="form-check-label" for="uispIgnoreCalculatedCapacity">Ignore Calculated Capacity</label>
<div class="form-text">Ignore capacity calculations from UISP</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
</div>
<script src="config_uisp.js"></script>

View File

@ -0,0 +1,106 @@
<div class="row">
<div class="col-12">
<ul class="config-menu">
<li class="config-menu-item"><a href="config_general.html" class="text-decoration-none"><i class="fa fa-server"></i> General</a></li>
<li class="config-menu-item"><a href="config_anon.html" class="text-decoration-none"><i class="fa fa-user-secret"></i> Anonymous Usage Stats</a></li>
<li class="config-menu-item"><a href="config_tuning.html" class="text-decoration-none"><i class="fa fa-warning"></i> Tuning</a></li>
<li class="config-menu-item"><a href="config_interface.html" class="text-decoration-none"><i class="fa fa-chain"></i> Network Mode</a></li>
<li class="config-menu-item"><a href="config_queues.html" class="text-decoration-none"><i class="fa fa-car"></i> Queues</a></li>
<li class="config-menu-item"><a href="config_lts.html" class="text-decoration-none"><i class="fa fa-line-chart"></i> Long-Term Stats</a></li>
<li class="config-menu-item"><a href="config_iprange.html" class="text-decoration-none"><i class="fa fa-address-card"></i> IP Ranges</a></li>
<li class="config-menu-item"><a href="config_flows.html" class="text-decoration-none"><i class="fa fa-arrow-circle-down"></i> Flow Tracking</a></li>
<li class="config-menu-item"><a href="config_integration.html" class="text-decoration-none"><i class="fa fa-link"></i> Integration - Common</a></li>
<li class="config-menu-item"><a href="config_spylnx.html" class="text-decoration-none"><i class="fa fa-link"></i> Splynx</a></li>
<li class="config-menu-item"><a href="config_uisp.html" class="text-decoration-none"><i class="fa fa-link"></i> UISP</a></li>
<li class="config-menu-item"><a href="config_powercode.html" class="text-decoration-none"><i class="fa fa-link"></i> Powercode</a></li>
<li class="config-menu-item"><a href="config_sonar.html" class="text-decoration-none"><i class="fa fa-link"></i> Sonar</a></li>
<li class="config-menu-item"><a href="config_network.html" class="text-decoration-none"><i class="fa fa-map"></i> Network Layout</a></li>
<li class="config-menu-item"><a href="config_devices.html" class="text-decoration-none"><i class="fa fa-table"></i> Shaped Devices</a></li>
<li class="config-menu-item active"><a href="config_users.html" class="text-decoration-none"><i class="fa fa-users"></i> LibreQoS Users</a></li>
</ul>
<hr class="mt-3 mb-3" />
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4>Manage Users</h4>
</div>
<div class="card-body">
<div id="users-list">
<!-- Users will be populated here by JavaScript -->
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<hr>
<h5>Add New User</h5>
<form id="add-user-form">
<div class="row mb-3">
<div class="col-md-4">
<label for="add-username" class="form-label">Username</label>
<input type="text" class="form-control" id="add-username" required>
</div>
<div class="col-md-4">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required>
</div>
<div class="col-md-4">
<label for="role" class="form-label">Role</label>
<select class="form-select" id="role" required>
<option value="Admin">Admin</option>
<option value="ReadOnly">Read Only</option>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fa fa-plus"></i> Add User
</button>
</form>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editUserModalLabel">Edit User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="edit-user-form">
<div class="mb-3">
<label for="edit-username" class="form-label">Username</label>
<input type="text" class="form-control" id="edit-username" readonly>
</div>
<div class="mb-3">
<label for="edit-password" class="form-label">Password</label>
<input type="password" class="form-control" id="edit-password">
<div class="form-text">Leave blank to keep current password</div>
</div>
<div class="mb-3">
<label for="edit-role" class="form-label">Role</label>
<select class="form-select" id="edit-role">
<option value="Admin">Admin</option>
<option value="ReadOnly">Read Only</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="save-user-changes">Save changes</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="config_users.js"></script>

View File

@ -184,6 +184,33 @@ table tr td a {
font-family: "Illegible";
src: url(glyphz.ttf) format("truetype");
}
.config-menu {
list-style-type: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.config-menu-item {
margin: 0;
padding: 5px 10px;
background-color: var(--bs-tertiary-bg);
border-radius: 4px;
cursor: pointer;
}
.config-menu-item:hover {
background-color: var(--bs-info-bg-subtle);
}
.config-menu-item.active {
background-color: var(--bs-info);
color: var(--bs-info-text);
font-weight: bold;
}
.toasty {
border-radius: 2px;
font-size: smaller;

View File

@ -90,7 +90,7 @@
%%LTS_LINK%%
<!-- Config -->
<li class="nav-item">
<a class="nav-link" href="configuration.html">
<a class="nav-link" href="config_general.html">
<i class="fa fa-fw fa-centerline fa-gears nav-icon"></i> Configuration
</a>
</li>

View File

@ -34,6 +34,22 @@ pub(super) fn static_routes() -> Result<Router> {
"circuit.html", "flow_map.html", "all_tree_sankey.html",
"asn_explorer.html", "lts_trial.html", "lts_trial_success.html",
"lts_trial_fail.html",
"config_general.html",
"config_anon.html",
"config_tuning.html",
"config_queues.html",
"config_lts.html",
"config_iprange.html",
"config_flows.html",
"config_integration.html",
"config_spylnx.html",
"config_uisp.html",
"config_powercode.html",
"config_sonar.html",
"config_interface.html",
"config_network.html",
"config_devices.html",
"config_users.html",
];
// Iterate through pages and construct the router

View File

@ -53,6 +53,18 @@ pub(crate) fn submit_throughput_stats(
counter: u8,
system_usage_actor: crossbeam_channel::Sender<tokio::sync::oneshot::Sender<SystemStats>>,
) {
let config = load_config();
if config.is_err() {
return;
}
let config = config.unwrap();
if config.long_term_stats.gather_stats == false {
return;
}
if config.long_term_stats.license_key.is_none() {
return;
}
let mut metrics = LtsSubmitMetrics::new();
let mut lts2_needs_shaped_devices = false;
// If ShapedDevices has changed, notify the stats thread

View File

@ -128,9 +128,6 @@ pub async fn build_full_network(
// Do Link Squashing
squash_single_aps(&mut sites)?;
// Build Path Weights
walk_tree_for_routing(&mut sites, &root_site, &routing_overrides)?;
// Apply bandwidth overrides
apply_bandwidth_overrides(&mut sites, &bandwidth_overrides);
@ -140,6 +137,9 @@ pub async fn build_full_network(
// Squash any sites that are in the squash list
squash_squashed_sites(&mut sites, config.clone(), &root_site)?;
// Build Path Weights
walk_tree_for_routing(config.clone(), &mut sites, &root_site, &routing_overrides)?;
// Print Sites
if let Some(root_idx) = sites.iter().position(|s| s.name == root_site) {
// Issue No Parent Warnings

View File

@ -53,12 +53,18 @@ fn parse_sites(sites_raw: &[Site], config: &Config) -> Vec<UispSite> {
}
fn parse_data_links(data_links_raw: &[DataLink]) -> Vec<UispDataLink> {
let data_links: Vec<UispDataLink> = data_links_raw
.iter()
.filter_map(UispDataLink::from_uisp)
.collect();
// We need to preserve symmetry, so each link is added twice
let mut data_links = Vec::with_capacity(data_links_raw.len() * 2);
for link in data_links_raw {
let uisp_link = UispDataLink::from_uisp(link);
if let Some(link) = uisp_link {
data_links.push(link.invert());
data_links.push(link);
}
}
info!(
"{} data-links have been successfully parsed",
"{} data-links have been successfully parsed (doubled)",
data_links.len()
);
data_links

View File

@ -3,6 +3,7 @@ use csv::ReaderBuilder;
use lqos_config::Config;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
use tracing::{error, info};
/// Represents a route override in the integrationUISProutes.csv file.
@ -82,3 +83,14 @@ pub fn get_route_overrides(config: &Config) -> Result<Vec<RouteOverride>, UispIn
Ok(Vec::new())
}
}
pub fn write_routing_overrides_template(config: Arc<Config>, natural_routes: &[RouteOverride]) -> anyhow::Result<()> {
let file_path = Path::new(&config.lqos_directory).join("integrationUISProutes.template.csv");
let mut writer = csv::Writer::from_path(file_path)?;
writer.write_record(&["From Site", "To Site", "Cost"])?;
for route in natural_routes {
writer.write_record(&[&route.from_site, &route.to_site, &route.cost.to_string()])?;
}
writer.flush()?;
Ok(())
}

View File

@ -1,6 +1,9 @@
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::sync::Arc;
use lqos_config::Config;
use crate::errors::UispIntegrationError;
use crate::strategies::full::routes_override::RouteOverride;
use crate::strategies::full::routes_override::{write_routing_overrides_template, RouteOverride};
use crate::uisp_types::{UispSite, UispSiteType};
/// Walks the tree to determine the best route for each site
@ -12,15 +15,85 @@ use crate::uisp_types::{UispSite, UispSiteType};
/// * `root_site` - The name of the root site
/// * `overrides` - The list of route overrides
pub fn walk_tree_for_routing(
config: Arc<Config>,
sites: &mut Vec<UispSite>,
root_site: &str,
overrides: &Vec<RouteOverride>,
) -> Result<(), UispIntegrationError> {
if let Some(root_idx) = sites.iter().position(|s| s.name == root_site) {
// Initialize the visualization
let mut dot_graph = "digraph G {\n graph [ ranksep=2.0 overlap=false ]\n".to_string();
// Make sure we know where the root is
let Some(root_idx) = sites.iter().position(|s| s.name == root_site) else {
tracing::error!("Unable to build a path-weights graph because I can't find the root node");
return Err(UispIntegrationError::NoRootSite);
};
// Now we iterate through every node that ISN'T the root
for i in 0..sites.len() {
// Skip the root. It's not going anywhere.
if (i == root_idx) {
continue;
}
// We need to find the shortest path to the root
let parents = sites[i].parent_indices.clone();
for destination_idx in parents {
// Is there a route override?
if let Some(route_override) = overrides.iter().find(|o| o.from_site == sites[i].name && o.to_site == sites[destination_idx].name) {
sites[i].route_weights.push((destination_idx, route_override.cost));
continue;
}
// If there's a direct route, it makes sense to use it
if destination_idx == root_idx {
sites[i].route_weights.push((destination_idx, 10));
continue;
}
// There's no direct route, so we want to evaluate the shortest path
let mut visited = std::collections::HashSet::new();
let current_node = root_idx;
let mut dot_graph = "digraph G {\n graph [ ranksep=2.0 overlap=false ]".to_string();
walk_node(current_node, 10, sites, &mut visited, overrides, &mut dot_graph);
visited.insert(i); // Don't go back to where we came from
let weight = find_shortest_path(
destination_idx,
root_idx,
visited,
sites,
overrides,
10,
);
if let Some(shortest) = weight {
sites[i].route_weights.push((destination_idx, shortest));
}
}
}
// Apply the lowest weight route
let site_index = sites
.iter()
.enumerate()
.map(|(i, site)| (i, site.name.clone()))
.collect::<std::collections::HashMap<usize, String>>();
for site in sites.iter_mut() {
if site.site_type != UispSiteType::Root && !site.route_weights.is_empty() {
// Sort to find the lowest exit
site.route_weights.sort_by(|a, b| a.1.cmp(&b.1));
site.selected_parent = Some(site.route_weights[0].0);
}
// Plot it
for (i,(idx, weight)) in site.route_weights.iter().enumerate() {
let from = site_index.get(&idx).unwrap().clone();
let to = site.name.clone();
if i == 0 {
dot_graph.push_str(&format!("\"{}\" -> \"{}\" [label=\"{}\" color=\"red\"] \n", from, to, weight));
} else {
dot_graph.push_str(&format!("\"{}\" -> \"{}\" [label=\"{}\"] \n", from, to, weight));
}
}
}
dot_graph.push_str("}\n");
{
let graph_file = std::fs::File::create("graph.dot");
@ -28,53 +101,45 @@ pub fn walk_tree_for_routing(
let _ = file.write_all(dot_graph.as_bytes());
}
}
} else {
tracing::error!("Unable to build a path-weights graph because I can't find the root node");
return Err(UispIntegrationError::NoRootSite);
}
// Apply the lowest weight route
for site in sites.iter_mut() {
if site.site_type != UispSiteType::Root && !site.route_weights.is_empty() {
// Sort to find the lowest exit
site.route_weights.sort_by(|a, b| a.1.cmp(&b.1));
site.selected_parent = Some(site.route_weights[0].0);
}
}
Ok(())
}
fn walk_node(
idx: usize,
weight: u32,
fn find_shortest_path(
from_idx: usize,
root_idx: usize,
mut visited: HashSet<usize>,
sites: &mut Vec<UispSite>,
visited: &mut std::collections::HashSet<usize>,
overrides: &Vec<RouteOverride>,
dot_graph: &mut String,
) {
if visited.contains(&idx) {
return;
weight: u32,
) -> Option<u32> {
// Make sure we don't loop
if visited.contains(&from_idx) {
return None;
}
visited.insert(idx);
for i in 0..sites.len() {
if sites[i].parent_indices.contains(&idx) {
let from = sites[i].name.clone();
let to = sites[idx].name.clone();
if sites[idx].site_type != UispSiteType::Client
{
dot_graph.push_str(&format!("\"{}\" [label=\"{}\"];\n", to, to));
visited.insert(from_idx);
let destinations = sites[from_idx].parent_indices.clone();
for destination_idx in destinations {
// Is there a route override?
if let Some(route_override) = overrides.iter().find(|o| o.from_site == sites[from_idx].name && o.to_site == sites[destination_idx].name) {
return Some(route_override.cost);
}
if let Some(route_override) = overrides
.iter()
.find(|o| (o.from_site == from && o.to_site == to) || (o.from_site == to && o.to_site == from))
{
sites[i].route_weights.push((idx, route_override.cost));
tracing::info!("Applied route override {} - {}", route_override.from_site, route_override.to_site);
} else {
sites[i].route_weights.push((idx, weight));
// If there's a direct route, go that way
if destination_idx == root_idx {
return Some(weight + 10);
}
walk_node(i, weight + 10, sites, visited, overrides, dot_graph);
// Don't go back to where we came from
if visited.contains(&destination_idx) {
continue;
}
// Calculate the route
let new_weight = find_shortest_path(destination_idx, root_idx, visited.clone(), sites, overrides, weight + 10);
if let Some(new_weight) = new_weight {
return Some(new_weight);
}
}
None
}

View File

@ -11,6 +11,18 @@ pub struct UispDataLink {
}
impl UispDataLink {
/// Inverts a data-link to provide the recripocal link.
pub fn invert(&self) -> Self {
Self {
id: self.id.clone(),
from_site_id: self.to_site_id.clone(),
to_site_id: self.from_site_id.clone(),
from_site_name: self.to_site_name.clone(),
to_site_name: self.from_site_name.clone(),
can_delete: self.can_delete,
}
}
/// Converts a UISP DataLink into a UispDataLink.
///
/// # Arguments
@ -19,12 +31,12 @@ impl UispDataLink {
let mut from_site_id = String::new();
let mut to_site_id = String::new();
let mut to_site_name = String::new();
let from_site_name = String::new();
let mut from_site_name = String::new();
// Obvious Site Links
if let Some(from_site) = &value.from.site {
from_site_id = from_site.identification.id.clone();
to_site_id = from_site.identification.name.clone();
from_site_name = from_site.identification.name.clone();
}
if let Some(to_site) = &value.to.site {
to_site_id = to_site.identification.id.clone();