|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Virtual ISP Stack - Network Management Dashboard</title> |
|
<link rel="stylesheet" href="styles.css"> |
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> |
|
</head> |
|
<body> |
|
<div class="app-container"> |
|
|
|
<header class="header"> |
|
<div class="header-content"> |
|
<div class="logo"> |
|
<i class="fas fa-network-wired"></i> |
|
<h1>Virtual ISP Stack</h1> |
|
</div> |
|
<div class="header-status"> |
|
<div class="status-indicator" id="systemStatus"> |
|
<i class="fas fa-circle"></i> |
|
<span>System Status</span> |
|
</div> |
|
<div class="refresh-btn" onclick="refreshData()"> |
|
<i class="fas fa-sync-alt"></i> |
|
</div> |
|
</div> |
|
</div> |
|
</header> |
|
|
|
|
|
<nav class="sidebar"> |
|
<div class="nav-menu"> |
|
<div class="nav-item active" data-section="dashboard"> |
|
<i class="fas fa-tachometer-alt"></i> |
|
<span>Dashboard</span> |
|
</div> |
|
<div class="nav-item" data-section="dhcp"> |
|
<i class="fas fa-server"></i> |
|
<span>DHCP</span> |
|
</div> |
|
<div class="nav-item" data-section="nat"> |
|
<i class="fas fa-exchange-alt"></i> |
|
<span>NAT</span> |
|
</div> |
|
<div class="nav-item" data-section="firewall"> |
|
<i class="fas fa-shield-alt"></i> |
|
<span>Firewall</span> |
|
</div> |
|
<div class="nav-item" data-section="router"> |
|
<i class="fas fa-route"></i> |
|
<span>Router</span> |
|
</div> |
|
<div class="nav-item" data-section="bridge"> |
|
<i class="fas fa-link"></i> |
|
<span>Bridge</span> |
|
</div> |
|
<div class="nav-item" data-section="sessions"> |
|
<i class="fas fa-users"></i> |
|
<span>Sessions</span> |
|
</div> |
|
<div class="nav-item" data-section="logs"> |
|
<i class="fas fa-file-alt"></i> |
|
<span>Logs</span> |
|
</div> |
|
<div class="nav-item" data-section="vpn"> |
|
<i class="fas fa-shield-alt"></i> |
|
<span>VPN</span> |
|
</div> |
|
<div class="nav-item" data-section="config"> |
|
<i class="fas fa-cog"></i> |
|
<span>Config</span> |
|
</div> |
|
</div> |
|
</nav> |
|
|
|
|
|
<main class="main-content"> |
|
|
|
<section id="dashboard" class="content-section active"> |
|
<div class="section-header"> |
|
<h2>System Dashboard</h2> |
|
<p>Overview of Virtual ISP Stack components and performance</p> |
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
<div class="stat-card"> |
|
<div class="stat-icon"> |
|
<i class="fas fa-server"></i> |
|
</div> |
|
<div class="stat-content"> |
|
<h3 id="dhcpLeaseCount">0</h3> |
|
<p>DHCP Leases</p> |
|
</div> |
|
</div> |
|
|
|
<div class="stat-card"> |
|
<div class="stat-icon"> |
|
<i class="fas fa-exchange-alt"></i> |
|
</div> |
|
<div class="stat-content"> |
|
<h3 id="natSessionCount">0</h3> |
|
<p>NAT Sessions</p> |
|
</div> |
|
</div> |
|
|
|
<div class="stat-card"> |
|
<div class="stat-icon"> |
|
<i class="fas fa-shield-alt"></i> |
|
</div> |
|
<div class="stat-content"> |
|
<h3 id="firewallRuleCount">0</h3> |
|
<p>Firewall Rules</p> |
|
</div> |
|
</div> |
|
|
|
<div class="stat-card"> |
|
<div class="stat-icon"> |
|
<i class="fas fa-link"></i> |
|
</div> |
|
<div class="stat-content"> |
|
<h3 id="bridgeClientCount">0</h3> |
|
<p>Bridge Clients</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="component-status"> |
|
<h3>Component Status</h3> |
|
<div class="component-grid" id="componentStatus"> |
|
|
|
</div> |
|
</div> |
|
|
|
|
|
<div class="charts-container"> |
|
<div class="chart-card"> |
|
<h3>Network Traffic</h3> |
|
<canvas id="trafficChart"></canvas> |
|
</div> |
|
<div class="chart-card"> |
|
<h3>Connection Distribution</h3> |
|
<canvas id="connectionChart"></canvas> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="dhcp" class="content-section"> |
|
<div class="section-header"> |
|
<h2>DHCP Management</h2> |
|
<p>Manage DHCP leases and configuration</p> |
|
</div> |
|
|
|
<div class="table-container"> |
|
<div class="table-header"> |
|
<h3>Active Leases</h3> |
|
<button class="btn btn-secondary" onclick="refreshDHCPLeases()"> |
|
<i class="fas fa-sync-alt"></i> Refresh |
|
</button> |
|
</div> |
|
<div class="table-wrapper"> |
|
<table id="dhcpTable"> |
|
<thead> |
|
<tr> |
|
<th>MAC Address</th> |
|
<th>IP Address</th> |
|
<th>Lease Time</th> |
|
<th>Remaining</th> |
|
<th>State</th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="dhcpTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="nat" class="content-section"> |
|
<div class="section-header"> |
|
<h2>NAT Management</h2> |
|
<p>Network Address Translation sessions and statistics</p> |
|
</div> |
|
|
|
<div class="nat-stats"> |
|
<div class="stat-row"> |
|
<div class="stat-item"> |
|
<span class="stat-label">Active Sessions:</span> |
|
<span class="stat-value" id="natActiveSessions">0</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">Port Utilization:</span> |
|
<span class="stat-value" id="natPortUtilization">0%</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">Bytes Translated:</span> |
|
<span class="stat-value" id="natBytesTranslated">0</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="table-container"> |
|
<div class="table-header"> |
|
<h3>NAT Sessions</h3> |
|
<button class="btn btn-secondary" onclick="refreshNATSessions()"> |
|
<i class="fas fa-sync-alt"></i> Refresh |
|
</button> |
|
</div> |
|
<div class="table-wrapper"> |
|
<table id="natTable"> |
|
<thead> |
|
<tr> |
|
<th>Virtual IP:Port</th> |
|
<th>Real IP:Port</th> |
|
<th>Host IP:Port</th> |
|
<th>Protocol</th> |
|
<th>Duration</th> |
|
<th>Bytes In/Out</th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="natTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="firewall" class="content-section"> |
|
<div class="section-header"> |
|
<h2>Firewall Management</h2> |
|
<p>Configure firewall rules and monitor traffic</p> |
|
</div> |
|
|
|
<div class="firewall-controls"> |
|
<button class="btn btn-primary" onclick="showAddRuleModal()"> |
|
<i class="fas fa-plus"></i> Add Rule |
|
</button> |
|
<button class="btn btn-secondary" onclick="refreshFirewallRules()"> |
|
<i class="fas fa-sync-alt"></i> Refresh |
|
</button> |
|
</div> |
|
|
|
<div class="table-container"> |
|
<div class="table-header"> |
|
<h3>Firewall Rules</h3> |
|
</div> |
|
<div class="table-wrapper"> |
|
<table id="firewallTable"> |
|
<thead> |
|
<tr> |
|
<th>Priority</th> |
|
<th>Rule ID</th> |
|
<th>Action</th> |
|
<th>Direction</th> |
|
<th>Source</th> |
|
<th>Destination</th> |
|
<th>Protocol</th> |
|
<th>Hits</th> |
|
<th>Status</th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="firewallTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="router" class="content-section"> |
|
<div class="section-header"> |
|
<h2>Router Management</h2> |
|
<p>Routing table and network interfaces</p> |
|
</div> |
|
|
|
<div class="router-tabs"> |
|
<div class="tab-buttons"> |
|
<button class="tab-btn active" data-tab="routes">Routes</button> |
|
<button class="tab-btn" data-tab="interfaces">Interfaces</button> |
|
<button class="tab-btn" data-tab="arp">ARP Table</button> |
|
</div> |
|
|
|
<div class="tab-content"> |
|
<div id="routes" class="tab-pane active"> |
|
<div class="table-container"> |
|
<table id="routesTable"> |
|
<thead> |
|
<tr> |
|
<th>Destination</th> |
|
<th>Gateway</th> |
|
<th>Interface</th> |
|
<th>Metric</th> |
|
<th>Type</th> |
|
<th>Use Count</th> |
|
<th>Last Used</th> |
|
</tr> |
|
</thead> |
|
<tbody id="routesTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
|
|
<div id="interfaces" class="tab-pane"> |
|
<div class="table-container"> |
|
<table id="interfacesTable"> |
|
<thead> |
|
<tr> |
|
<th>Name</th> |
|
<th>IP Address</th> |
|
<th>Network</th> |
|
<th>MTU</th> |
|
<th>Status</th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="interfacesTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
|
|
<div id="arp" class="tab-pane"> |
|
<div class="table-container"> |
|
<table id="arpTable"> |
|
<thead> |
|
<tr> |
|
<th>IP Address</th> |
|
<th>MAC Address</th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="arpTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="bridge" class="content-section"> |
|
<div class="section-header"> |
|
<h2>Packet Bridge</h2> |
|
<p>Connected clients and bridge statistics</p> |
|
</div> |
|
|
|
<div class="bridge-info"> |
|
<div class="info-card"> |
|
<h4>WebSocket Server</h4> |
|
<p>Port: 8765</p> |
|
<p>Status: <span class="status-active">Active</span></p> |
|
</div> |
|
<div class="info-card"> |
|
<h4>TCP Server</h4> |
|
<p>Port: 8766</p> |
|
<p>Status: <span class="status-active">Active</span></p> |
|
</div> |
|
</div> |
|
|
|
<div class="table-container"> |
|
<div class="table-header"> |
|
<h3>Connected Clients</h3> |
|
<button class="btn btn-secondary" onclick="refreshBridgeClients()"> |
|
<i class="fas fa-sync-alt"></i> Refresh |
|
</button> |
|
</div> |
|
<div class="table-wrapper"> |
|
<table id="bridgeTable"> |
|
<thead> |
|
<tr> |
|
<th>Client ID</th> |
|
<th>Type</th> |
|
<th>Remote Address</th> |
|
<th>Connected Time</th> |
|
<th>Packets In/Out</th> |
|
<th>Bytes In/Out</th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="bridgeTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="sessions" class="content-section"> |
|
<div class="section-header"> |
|
<h2>Session Tracking</h2> |
|
<p>Unified view of all network sessions</p> |
|
</div> |
|
|
|
<div class="session-summary" id="sessionSummary"> |
|
|
|
</div> |
|
|
|
<div class="table-container"> |
|
<div class="table-header"> |
|
<h3>Active Sessions</h3> |
|
<div class="table-controls"> |
|
<select id="sessionTypeFilter" onchange="filterSessions()"> |
|
<option value="">All Types</option> |
|
<option value="DHCP_LEASE">DHCP Lease</option> |
|
<option value="NAT_SESSION">NAT Session</option> |
|
<option value="TCP_CONNECTION">TCP Connection</option> |
|
<option value="SOCKET_CONNECTION">Socket Connection</option> |
|
<option value="BRIDGE_CLIENT">Bridge Client</option> |
|
</select> |
|
<button class="btn btn-secondary" onclick="refreshSessions()"> |
|
<i class="fas fa-sync-alt"></i> Refresh |
|
</button> |
|
</div> |
|
</div> |
|
<div class="table-wrapper"> |
|
<table id="sessionsTable"> |
|
<thead> |
|
<tr> |
|
<th>Session ID</th> |
|
<th>Type</th> |
|
<th>State</th> |
|
<th>Virtual IP:Port</th> |
|
<th>Real IP:Port</th> |
|
<th>Protocol</th> |
|
<th>Duration</th> |
|
<th>Idle Time</th> |
|
<th>Metrics</th> |
|
</tr> |
|
</thead> |
|
<tbody id="sessionsTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="logs" class="content-section"> |
|
<div class="section-header"> |
|
<h2>System Logs</h2> |
|
<p>Monitor system events and troubleshoot issues</p> |
|
</div> |
|
|
|
<div class="log-controls"> |
|
<div class="log-filters"> |
|
<select id="logLevelFilter" onchange="filterLogs()"> |
|
<option value="">All Levels</option> |
|
<option value="DEBUG">Debug</option> |
|
<option value="INFO">Info</option> |
|
<option value="WARNING">Warning</option> |
|
<option value="ERROR">Error</option> |
|
<option value="CRITICAL">Critical</option> |
|
</select> |
|
<select id="logCategoryFilter" onchange="filterLogs()"> |
|
<option value="">All Categories</option> |
|
<option value="SYSTEM">System</option> |
|
<option value="DHCP">DHCP</option> |
|
<option value="NAT">NAT</option> |
|
<option value="FIREWALL">Firewall</option> |
|
<option value="TCP">TCP</option> |
|
<option value="ROUTER">Router</option> |
|
<option value="BRIDGE">Bridge</option> |
|
<option value="SOCKET">Socket</option> |
|
<option value="SESSION">Session</option> |
|
<option value="SECURITY">Security</option> |
|
</select> |
|
<input type="text" id="logSearchInput" placeholder="Search logs..." onkeyup="searchLogs()"> |
|
</div> |
|
<div class="log-actions"> |
|
<button class="btn btn-secondary" onclick="refreshLogs()"> |
|
<i class="fas fa-sync-alt"></i> Refresh |
|
</button> |
|
<button class="btn btn-danger" onclick="clearLogs()"> |
|
<i class="fas fa-trash"></i> Clear |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="log-container" id="logContainer"> |
|
|
|
</div> |
|
</section> |
|
|
|
|
|
<section id="vpn" class="content-section"> |
|
<div class="section-header"> |
|
<h2>VPN Management</h2> |
|
<p>OpenVPN server management and client connections</p> |
|
</div> |
|
|
|
|
|
<div class="vpn-status"> |
|
<div class="status-card"> |
|
<div class="status-header"> |
|
<h3>OpenVPN Server</h3> |
|
<div class="server-controls"> |
|
<button class="btn btn-success" id="startVpnBtn" onclick="startVpnServer()"> |
|
<i class="fas fa-play"></i> Start |
|
</button> |
|
<button class="btn btn-danger" id="stopVpnBtn" onclick="stopVpnServer()"> |
|
<i class="fas fa-stop"></i> Stop |
|
</button> |
|
<button class="btn btn-secondary" onclick="refreshVpnStatus()"> |
|
<i class="fas fa-sync-alt"></i> Refresh |
|
</button> |
|
</div> |
|
</div> |
|
<div class="status-info"> |
|
<div class="info-row"> |
|
<span class="info-label">Status:</span> |
|
<span class="info-value" id="vpnServerStatus">Unknown</span> |
|
</div> |
|
<div class="info-row"> |
|
<span class="info-label">Server IP:</span> |
|
<span class="info-value" id="vpnServerIp">-</span> |
|
</div> |
|
<div class="info-row"> |
|
<span class="info-label">Port:</span> |
|
<span class="info-value" id="vpnServerPort">-</span> |
|
</div> |
|
<div class="info-row"> |
|
<span class="info-label">Connected Clients:</span> |
|
<span class="info-value" id="vpnConnectedClients">0</span> |
|
</div> |
|
<div class="info-row"> |
|
<span class="info-label">Uptime:</span> |
|
<span class="info-value" id="vpnUptime">-</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="vpn-stats"> |
|
<div class="stat-item"> |
|
<span class="stat-label">Total Bytes Received:</span> |
|
<span class="stat-value" id="vpnBytesReceived">0</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">Total Bytes Sent:</span> |
|
<span class="stat-value" id="vpnBytesSent">0</span> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="table-container"> |
|
<div class="table-header"> |
|
<h3>Connected VPN Clients</h3> |
|
<div class="table-controls"> |
|
<button class="btn btn-primary" onclick="showGenerateConfigModal()"> |
|
<i class="fas fa-plus"></i> Generate Client Config |
|
</button> |
|
<button class="btn btn-secondary" onclick="refreshVpnClients()"> |
|
<i class="fas fa-sync-alt"></i> Refresh |
|
</button> |
|
</div> |
|
</div> |
|
<div class="table-wrapper"> |
|
<table id="vpnClientsTable"> |
|
<thead> |
|
<tr> |
|
<th>Client ID</th> |
|
<th>Common Name</th> |
|
<th>VPN IP Address</th> |
|
<th>Connected Since</th> |
|
<th>Bytes Received</th> |
|
<th>Bytes Sent</th> |
|
<th>Status</th> |
|
<th>Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="vpnClientsTableBody"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="config" class="content-section"> |
|
<div class="section-header"> |
|
<h2>System Configuration</h2> |
|
<p>Configure system parameters and settings</p> |
|
</div> |
|
|
|
<div class="config-container"> |
|
<div class="config-section"> |
|
<h3>DHCP Configuration</h3> |
|
<div class="config-form" id="dhcpConfig"> |
|
|
|
</div> |
|
</div> |
|
|
|
<div class="config-section"> |
|
<h3>NAT Configuration</h3> |
|
<div class="config-form" id="natConfig"> |
|
|
|
</div> |
|
</div> |
|
|
|
<div class="config-section"> |
|
<h3>Firewall Configuration</h3> |
|
<div class="config-form" id="firewallConfig"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="config-actions"> |
|
<button class="btn btn-primary" onclick="saveConfiguration()"> |
|
<i class="fas fa-save"></i> Save Configuration |
|
</button> |
|
<button class="btn btn-secondary" onclick="resetConfiguration()"> |
|
<i class="fas fa-undo"></i> Reset to Defaults |
|
</button> |
|
</div> |
|
</section> |
|
</main> |
|
</div> |
|
|
|
|
|
<div id="addRuleModal" class="modal"> |
|
<div class="modal-content"> |
|
<div class="modal-header"> |
|
<h3>Add Firewall Rule</h3> |
|
<span class="close" onclick="closeModal('addRuleModal')">×</span> |
|
</div> |
|
<div class="modal-body"> |
|
<form id="addRuleForm"> |
|
<div class="form-group"> |
|
<label for="ruleId">Rule ID:</label> |
|
<input type="text" id="ruleId" name="ruleId" required> |
|
</div> |
|
<div class="form-group"> |
|
<label for="rulePriority">Priority:</label> |
|
<input type="number" id="rulePriority" name="priority" value="100" required> |
|
</div> |
|
<div class="form-group"> |
|
<label for="ruleAction">Action:</label> |
|
<select id="ruleAction" name="action" required> |
|
<option value="ACCEPT">Accept</option> |
|
<option value="DROP">Drop</option> |
|
<option value="REJECT">Reject</option> |
|
</select> |
|
</div> |
|
<div class="form-group"> |
|
<label for="ruleDirection">Direction:</label> |
|
<select id="ruleDirection" name="direction"> |
|
<option value="BOTH">Both</option> |
|
<option value="INBOUND">Inbound</option> |
|
<option value="OUTBOUND">Outbound</option> |
|
</select> |
|
</div> |
|
<div class="form-group"> |
|
<label for="ruleSourceIp">Source IP:</label> |
|
<input type="text" id="ruleSourceIp" name="source_ip" placeholder="e.g., 192.168.1.0/24"> |
|
</div> |
|
<div class="form-group"> |
|
<label for="ruleDestIp">Destination IP:</label> |
|
<input type="text" id="ruleDestIp" name="dest_ip" placeholder="e.g., 10.0.0.0/8"> |
|
</div> |
|
<div class="form-group"> |
|
<label for="ruleSourcePort">Source Port:</label> |
|
<input type="text" id="ruleSourcePort" name="source_port" placeholder="e.g., 80, 80-90, 80,443"> |
|
</div> |
|
<div class="form-group"> |
|
<label for="ruleDestPort">Destination Port:</label> |
|
<input type="text" id="ruleDestPort" name="dest_port" placeholder="e.g., 80, 80-90, 80,443"> |
|
</div> |
|
<div class="form-group"> |
|
<label for="ruleProtocol">Protocol:</label> |
|
<select id="ruleProtocol" name="protocol"> |
|
<option value="">Any</option> |
|
<option value="TCP">TCP</option> |
|
<option value="UDP">UDP</option> |
|
<option value="ICMP">ICMP</option> |
|
</select> |
|
</div> |
|
<div class="form-group"> |
|
<label for="ruleDescription">Description:</label> |
|
<input type="text" id="ruleDescription" name="description" placeholder="Rule description"> |
|
</div> |
|
</form> |
|
</div> |
|
<div class="modal-footer"> |
|
<button type="button" class="btn btn-secondary" onclick="closeModal('addRuleModal')">Cancel</button> |
|
<button type="button" class="btn btn-primary" onclick="addFirewallRule()">Add Rule</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="generateConfigModal" class="modal"> |
|
<div class="modal-content"> |
|
<div class="modal-header"> |
|
<h3>Generate VPN Client Configuration</h3> |
|
<span class="close" onclick="closeModal('generateConfigModal')">×</span> |
|
</div> |
|
<div class="modal-body"> |
|
<form id="generateConfigForm"> |
|
<div class="form-group"> |
|
<label for="clientName">Client Name:</label> |
|
<input type="text" id="clientName" name="clientName" required |
|
placeholder="Enter client name (e.g., client1)"> |
|
</div> |
|
<div class="form-group"> |
|
<label for="serverIp">Server IP:</label> |
|
<input type="text" id="serverIp" name="serverIp" required |
|
placeholder="Enter server IP address"> |
|
</div> |
|
</form> |
|
</div> |
|
<div class="modal-footer"> |
|
<button type="button" class="btn btn-secondary" onclick="closeModal('generateConfigModal')">Cancel</button> |
|
<button type="button" class="btn btn-primary" onclick="generateClientConfig()">Generate Config</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="loadingOverlay" class="loading-overlay"> |
|
<div class="loading-spinner"> |
|
<i class="fas fa-spinner fa-spin"></i> |
|
<p>Loading...</p> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="toastContainer" class="toast-container"></div> |
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
|
<script src="app.js"></script> |
|
</body> |
|
</html> |
|
|
|
|