mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
Merge branch 'develop' into insightful_ui
# Conflicts: # src/rust/lqosd/src/node_manager/static2/node_manager.css
This commit is contained in:
commit
011b2ce971
@ -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.
|
||||
|
@ -1 +1 @@
|
||||
1.5-BETA7
|
||||
1.5-BETA8
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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}"
|
||||
|
@ -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;
|
||||
}
|
42
src/rust/lqosd/src/node_manager/js_build/src/config_anon.js
Normal file
42
src/rust/lqosd/src/node_manager/js_build/src/config_anon.js
Normal 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");
|
||||
}
|
||||
});
|
477
src/rust/lqosd/src/node_manager/js_build/src/config_devices.js
Normal file
477
src/rust/lqosd/src/node_manager/js_build/src/config_devices.js
Normal 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);
|
93
src/rust/lqosd/src/node_manager/js_build/src/config_flows.js
Normal file
93
src/rust/lqosd/src/node_manager/js_build/src/config_flows.js
Normal 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");
|
||||
}
|
||||
});
|
@ -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");
|
||||
}
|
||||
});
|
@ -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");
|
||||
}
|
||||
});
|
101
src/rust/lqosd/src/node_manager/js_build/src/config_interface.js
Normal file
101
src/rust/lqosd/src/node_manager/js_build/src/config_interface.js
Normal 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");
|
||||
}
|
||||
});
|
131
src/rust/lqosd/src/node_manager/js_build/src/config_iprange.js
Normal file
131
src/rust/lqosd/src/node_manager/js_build/src/config_iprange.js
Normal 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");
|
||||
}
|
||||
});
|
73
src/rust/lqosd/src/node_manager/js_build/src/config_lts.js
Normal file
73
src/rust/lqosd/src/node_manager/js_build/src/config_lts.js
Normal 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");
|
||||
}
|
||||
});
|
273
src/rust/lqosd/src/node_manager/js_build/src/config_network.js
Normal file
273
src/rust/lqosd/src/node_manager/js_build/src/config_network.js
Normal 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);
|
@ -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");
|
||||
}
|
||||
});
|
@ -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");
|
||||
}
|
||||
});
|
92
src/rust/lqosd/src/node_manager/js_build/src/config_sonar.js
Normal file
92
src/rust/lqosd/src/node_manager/js_build/src/config_sonar.js
Normal 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");
|
||||
}
|
||||
});
|
@ -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");
|
||||
}
|
||||
});
|
@ -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");
|
||||
}
|
||||
});
|
130
src/rust/lqosd/src/node_manager/js_build/src/config_uisp.js
Normal file
130
src/rust/lqosd/src/node_manager/js_build/src/config_uisp.js
Normal 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");
|
||||
}
|
||||
});
|
128
src/rust/lqosd/src/node_manager/js_build/src/config_users.js
Normal file
128
src/rust/lqosd/src/node_manager/js_build/src/config_users.js
Normal 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>');
|
||||
});
|
||||
}
|
@ -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) => {
|
||||
|
@ -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))
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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()
|
||||
|
44
src/rust/lqosd/src/node_manager/static2/config_anon.html
Normal file
44
src/rust/lqosd/src/node_manager/static2/config_anon.html
Normal 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>
|
35
src/rust/lqosd/src/node_manager/static2/config_devices.html
Normal file
35
src/rust/lqosd/src/node_manager/static2/config_devices.html
Normal 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>
|
79
src/rust/lqosd/src/node_manager/static2/config_flows.html
Normal file
79
src/rust/lqosd/src/node_manager/static2/config_flows.html
Normal 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>
|
65
src/rust/lqosd/src/node_manager/static2/config_general.html
Normal file
65
src/rust/lqosd/src/node_manager/static2/config_general.html
Normal 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>
|
@ -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>
|
125
src/rust/lqosd/src/node_manager/static2/config_interface.html
Normal file
125
src/rust/lqosd/src/node_manager/static2/config_interface.html
Normal 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>
|
89
src/rust/lqosd/src/node_manager/static2/config_iprange.html
Normal file
89
src/rust/lqosd/src/node_manager/static2/config_iprange.html
Normal 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>
|
68
src/rust/lqosd/src/node_manager/static2/config_lts.html
Normal file
68
src/rust/lqosd/src/node_manager/static2/config_lts.html
Normal 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>
|
36
src/rust/lqosd/src/node_manager/static2/config_network.html
Normal file
36
src/rust/lqosd/src/node_manager/static2/config_network.html
Normal 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>
|
@ -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>
|
96
src/rust/lqosd/src/node_manager/static2/config_queues.html
Normal file
96
src/rust/lqosd/src/node_manager/static2/config_queues.html
Normal 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>
|
74
src/rust/lqosd/src/node_manager/static2/config_sonar.html
Normal file
74
src/rust/lqosd/src/node_manager/static2/config_sonar.html
Normal 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>
|
55
src/rust/lqosd/src/node_manager/static2/config_spylnx.html
Normal file
55
src/rust/lqosd/src/node_manager/static2/config_spylnx.html
Normal 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>
|
81
src/rust/lqosd/src/node_manager/static2/config_tuning.html
Normal file
81
src/rust/lqosd/src/node_manager/static2/config_tuning.html
Normal 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>
|
110
src/rust/lqosd/src/node_manager/static2/config_uisp.html
Normal file
110
src/rust/lqosd/src/node_manager/static2/config_uisp.html
Normal 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>
|
106
src/rust/lqosd/src/node_manager/static2/config_users.html
Normal file
106
src/rust/lqosd/src/node_manager/static2/config_users.html
Normal 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>
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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,69 +15,131 @@ 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) {
|
||||
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);
|
||||
dot_graph.push_str("}\n");
|
||||
{
|
||||
let graph_file = std::fs::File::create("graph.dot");
|
||||
if let Ok(mut file) = graph_file {
|
||||
let _ = file.write_all(dot_graph.as_bytes());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// 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();
|
||||
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");
|
||||
if let Ok(mut file) = graph_file {
|
||||
let _ = file.write_all(dot_graph.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
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));
|
||||
}
|
||||
walk_node(i, weight + 10, sites, visited, overrides, dot_graph);
|
||||
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 there's a direct route, go that way
|
||||
if destination_idx == root_idx {
|
||||
return Some(weight + 10);
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user