|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VirtualISPApp { |
|
|
constructor() { |
|
|
this.apiBase = '/api'; |
|
|
this.currentSection = 'dashboard'; |
|
|
this.refreshInterval = null; |
|
|
this.charts = {}; |
|
|
|
|
|
this.init(); |
|
|
} |
|
|
|
|
|
async init() { |
|
|
this.setupEventListeners(); |
|
|
this.setupNavigation(); |
|
|
this.setupCharts(); |
|
|
await this.loadInitialData(); |
|
|
this.startAutoRefresh(); |
|
|
|
|
|
|
|
|
this.hideLoading(); |
|
|
|
|
|
console.log('Virtual ISP Stack App initialized'); |
|
|
} |
|
|
|
|
|
setupEventListeners() { |
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => { |
|
|
item.addEventListener('click', (e) => { |
|
|
const section = e.currentTarget.dataset.section; |
|
|
this.navigateToSection(section); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(btn => { |
|
|
btn.addEventListener('click', (e) => { |
|
|
const tab = e.currentTarget.dataset.tab; |
|
|
this.switchTab(tab); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.close').forEach(btn => { |
|
|
btn.addEventListener('click', (e) => { |
|
|
const modal = e.currentTarget.closest('.modal'); |
|
|
this.closeModal(modal.id); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.modal').forEach(modal => { |
|
|
modal.addEventListener('click', (e) => { |
|
|
if (e.target === modal) { |
|
|
this.closeModal(modal.id); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('addRuleForm')?.addEventListener('submit', (e) => { |
|
|
e.preventDefault(); |
|
|
this.addFirewallRule(); |
|
|
}); |
|
|
} |
|
|
|
|
|
setupNavigation() { |
|
|
|
|
|
this.navigateToSection('dashboard'); |
|
|
} |
|
|
|
|
|
navigateToSection(section) { |
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => { |
|
|
item.classList.remove('active'); |
|
|
}); |
|
|
document.querySelector(`[data-section="${section}"]`).classList.add('active'); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.content-section').forEach(sec => { |
|
|
sec.classList.remove('active'); |
|
|
}); |
|
|
document.getElementById(section).classList.add('active'); |
|
|
|
|
|
this.currentSection = section; |
|
|
|
|
|
|
|
|
this.loadSectionData(section); |
|
|
} |
|
|
|
|
|
switchTab(tab) { |
|
|
const container = event.target.closest('.router-tabs'); |
|
|
|
|
|
|
|
|
container.querySelectorAll('.tab-btn').forEach(btn => { |
|
|
btn.classList.remove('active'); |
|
|
}); |
|
|
event.target.classList.add('active'); |
|
|
|
|
|
|
|
|
container.querySelectorAll('.tab-pane').forEach(pane => { |
|
|
pane.classList.remove('active'); |
|
|
}); |
|
|
container.querySelector(`#${tab}`).classList.add('active'); |
|
|
|
|
|
|
|
|
this.loadTabData(tab); |
|
|
} |
|
|
|
|
|
async loadInitialData() { |
|
|
try { |
|
|
await Promise.all([ |
|
|
this.loadSystemStatus(), |
|
|
this.loadDashboardData(), |
|
|
this.loadConfiguration() |
|
|
]); |
|
|
} catch (error) { |
|
|
console.error('Error loading initial data:', error); |
|
|
this.showToast('Error loading initial data', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async loadSectionData(section) { |
|
|
try { |
|
|
switch (section) { |
|
|
case 'dashboard': |
|
|
await this.loadDashboardData(); |
|
|
break; |
|
|
case 'dhcp': |
|
|
await this.loadDHCPData(); |
|
|
break; |
|
|
case 'nat': |
|
|
await this.loadNATData(); |
|
|
break; |
|
|
case 'firewall': |
|
|
await this.loadFirewallData(); |
|
|
break; |
|
|
case 'router': |
|
|
await this.loadRouterData(); |
|
|
break; |
|
|
case 'bridge': |
|
|
await this.loadBridgeData(); |
|
|
break; |
|
|
case 'sessions': |
|
|
await this.loadSessionsData(); |
|
|
break; |
|
|
case 'logs': |
|
|
await this.loadLogsData(); |
|
|
break; |
|
|
case 'config': |
|
|
await this.loadConfiguration(); |
|
|
break; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(`Error loading ${section} data:`, error); |
|
|
this.showToast(`Error loading ${section} data`, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async loadTabData(tab) { |
|
|
try { |
|
|
switch (tab) { |
|
|
case 'routes': |
|
|
await this.loadRoutingTable(); |
|
|
break; |
|
|
case 'interfaces': |
|
|
await this.loadInterfaces(); |
|
|
break; |
|
|
case 'arp': |
|
|
await this.loadARPTable(); |
|
|
break; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error(`Error loading ${tab} data:`, error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async apiCall(endpoint, options = {}) { |
|
|
const url = `${this.apiBase}${endpoint}`; |
|
|
const defaultOptions = { |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
}; |
|
|
|
|
|
const response = await fetch(url, { ...defaultOptions, ...options }); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`API call failed: ${response.status} ${response.statusText}`); |
|
|
} |
|
|
|
|
|
return await response.json(); |
|
|
} |
|
|
|
|
|
|
|
|
async loadSystemStatus() { |
|
|
try { |
|
|
const response = await this.apiCall('/status'); |
|
|
this.updateSystemStatus(response.system_status); |
|
|
} catch (error) { |
|
|
console.error('Error loading system status:', error); |
|
|
this.updateSystemStatusOffline(); |
|
|
} |
|
|
} |
|
|
|
|
|
updateSystemStatus(status) { |
|
|
const indicator = document.getElementById('systemStatus'); |
|
|
const components = status.components; |
|
|
|
|
|
|
|
|
const allOnline = Object.values(components).every(c => c === true); |
|
|
indicator.className = `status-indicator ${allOnline ? 'online' : 'offline'}`; |
|
|
indicator.querySelector('span').textContent = allOnline ? 'All Systems Online' : 'System Issues'; |
|
|
|
|
|
|
|
|
this.updateComponentStatus(components); |
|
|
} |
|
|
|
|
|
updateSystemStatusOffline() { |
|
|
const indicator = document.getElementById('systemStatus'); |
|
|
indicator.className = 'status-indicator offline'; |
|
|
indicator.querySelector('span').textContent = 'System Offline'; |
|
|
} |
|
|
|
|
|
updateComponentStatus(components) { |
|
|
const container = document.getElementById('componentStatus'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
Object.entries(components).forEach(([name, status]) => { |
|
|
const item = document.createElement('div'); |
|
|
item.className = `component-item ${status ? 'online' : 'offline'}`; |
|
|
item.innerHTML = ` |
|
|
<span class="component-name">${this.formatComponentName(name)}</span> |
|
|
<span class="component-status-badge ${status ? 'online' : 'offline'}"> |
|
|
${status ? 'Online' : 'Offline'} |
|
|
</span> |
|
|
`; |
|
|
container.appendChild(item); |
|
|
}); |
|
|
} |
|
|
|
|
|
formatComponentName(name) { |
|
|
return name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); |
|
|
} |
|
|
|
|
|
|
|
|
async loadDashboardData() { |
|
|
try { |
|
|
const [statusResponse, statsResponse] = await Promise.all([ |
|
|
this.apiCall('/status'), |
|
|
this.apiCall('/stats') |
|
|
]); |
|
|
|
|
|
this.updateDashboardStats(statusResponse.system_status.stats); |
|
|
this.updateCharts(statsResponse.stats); |
|
|
} catch (error) { |
|
|
console.error('Error loading dashboard data:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
updateDashboardStats(stats) { |
|
|
document.getElementById('dhcpLeaseCount').textContent = stats.dhcp_leases || 0; |
|
|
document.getElementById('natSessionCount').textContent = stats.nat_sessions || 0; |
|
|
document.getElementById('firewallRuleCount').textContent = stats.firewall_rules || 0; |
|
|
document.getElementById('bridgeClientCount').textContent = stats.bridge_clients || 0; |
|
|
} |
|
|
|
|
|
|
|
|
async loadDHCPData() { |
|
|
try { |
|
|
const response = await this.apiCall('/dhcp/leases'); |
|
|
this.updateDHCPTable(response.leases); |
|
|
} catch (error) { |
|
|
console.error('Error loading DHCP data:', error); |
|
|
this.updateDHCPTable([]); |
|
|
} |
|
|
} |
|
|
|
|
|
updateDHCPTable(leases) { |
|
|
const tbody = document.getElementById('dhcpTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
leases.forEach(lease => { |
|
|
const row = document.createElement('tr'); |
|
|
const remaining = Math.max(0, lease.lease_time - (Date.now() / 1000 - lease.lease_start)); |
|
|
|
|
|
row.innerHTML = ` |
|
|
<td>${lease.mac_address}</td> |
|
|
<td>${lease.ip_address}</td> |
|
|
<td>${this.formatDuration(lease.lease_time)}</td> |
|
|
<td>${this.formatDuration(remaining)}</td> |
|
|
<td><span class="status-badge status-${lease.state.toLowerCase()}">${lease.state}</span></td> |
|
|
<td> |
|
|
<button class="btn btn-danger btn-sm" onclick="app.releaseDHCPLease('${lease.mac_address}')"> |
|
|
<i class="fas fa-times"></i> Release |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
async releaseDHCPLease(macAddress) { |
|
|
try { |
|
|
await this.apiCall(`/dhcp/leases/${macAddress}`, { method: 'DELETE' }); |
|
|
this.showToast('DHCP lease released successfully', 'success'); |
|
|
await this.loadDHCPData(); |
|
|
} catch (error) { |
|
|
console.error('Error releasing DHCP lease:', error); |
|
|
this.showToast('Error releasing DHCP lease', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async loadNATData() { |
|
|
try { |
|
|
const [sessionsResponse, statsResponse] = await Promise.all([ |
|
|
this.apiCall('/nat/sessions'), |
|
|
this.apiCall('/nat/stats') |
|
|
]); |
|
|
|
|
|
this.updateNATStats(statsResponse.stats); |
|
|
this.updateNATTable(sessionsResponse.sessions); |
|
|
} catch (error) { |
|
|
console.error('Error loading NAT data:', error); |
|
|
this.updateNATStats({}); |
|
|
this.updateNATTable([]); |
|
|
} |
|
|
} |
|
|
|
|
|
updateNATStats(stats) { |
|
|
document.getElementById('natActiveSessions').textContent = stats.active_sessions || 0; |
|
|
document.getElementById('natPortUtilization').textContent = |
|
|
`${Math.round((stats.ports_used / stats.total_ports) * 100) || 0}%`; |
|
|
document.getElementById('natBytesTranslated').textContent = |
|
|
this.formatBytes(stats.bytes_translated || 0); |
|
|
} |
|
|
|
|
|
updateNATTable(sessions) { |
|
|
const tbody = document.getElementById('natTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
sessions.forEach(session => { |
|
|
const row = document.createElement('tr'); |
|
|
row.innerHTML = ` |
|
|
<td>${session.virtual_ip}:${session.virtual_port}</td> |
|
|
<td>${session.real_ip}:${session.real_port}</td> |
|
|
<td>${session.host_ip}:${session.host_port}</td> |
|
|
<td>${session.protocol}</td> |
|
|
<td>${this.formatDuration(session.duration)}</td> |
|
|
<td>${this.formatBytes(session.bytes_in)} / ${this.formatBytes(session.bytes_out)}</td> |
|
|
<td> |
|
|
<button class="btn btn-danger btn-sm" onclick="app.closeNATSession('${session.session_id}')"> |
|
|
<i class="fas fa-times"></i> Close |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async loadFirewallData() { |
|
|
try { |
|
|
const [rulesResponse, logsResponse, statsResponse] = await Promise.all([ |
|
|
this.apiCall('/firewall/rules'), |
|
|
this.apiCall('/firewall/logs?limit=50'), |
|
|
this.apiCall('/firewall/stats') |
|
|
]); |
|
|
|
|
|
this.updateFirewallTable(rulesResponse.rules); |
|
|
} catch (error) { |
|
|
console.error('Error loading firewall data:', error); |
|
|
this.updateFirewallTable([]); |
|
|
} |
|
|
} |
|
|
|
|
|
updateFirewallTable(rules) { |
|
|
const tbody = document.getElementById('firewallTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
rules.forEach(rule => { |
|
|
const row = document.createElement('tr'); |
|
|
row.innerHTML = ` |
|
|
<td>${rule.priority}</td> |
|
|
<td>${rule.rule_id}</td> |
|
|
<td><span class="status-badge status-${rule.action.toLowerCase()}">${rule.action}</span></td> |
|
|
<td>${rule.direction}</td> |
|
|
<td>${rule.source_ip || 'Any'}${rule.source_port ? ':' + rule.source_port : ''}</td> |
|
|
<td>${rule.dest_ip || 'Any'}${rule.dest_port ? ':' + rule.dest_port : ''}</td> |
|
|
<td>${rule.protocol || 'Any'}</td> |
|
|
<td>${rule.hit_count || 0}</td> |
|
|
<td><span class="status-badge status-${rule.enabled ? 'active' : 'inactive'}">${rule.enabled ? 'Enabled' : 'Disabled'}</span></td> |
|
|
<td> |
|
|
<button class="btn btn-danger btn-sm" onclick="app.deleteFirewallRule('${rule.rule_id}')"> |
|
|
<i class="fas fa-trash"></i> Delete |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
async deleteFirewallRule(ruleId) { |
|
|
try { |
|
|
await this.apiCall(`/firewall/rules/${ruleId}`, { method: 'DELETE' }); |
|
|
this.showToast('Firewall rule deleted successfully', 'success'); |
|
|
await this.loadFirewallData(); |
|
|
} catch (error) { |
|
|
console.error('Error deleting firewall rule:', error); |
|
|
this.showToast('Error deleting firewall rule', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async loadRouterData() { |
|
|
await Promise.all([ |
|
|
this.loadRoutingTable(), |
|
|
this.loadInterfaces(), |
|
|
this.loadARPTable() |
|
|
]); |
|
|
} |
|
|
|
|
|
async loadRoutingTable() { |
|
|
try { |
|
|
const response = await this.apiCall('/router/routes'); |
|
|
this.updateRoutingTable(response.routes); |
|
|
} catch (error) { |
|
|
console.error('Error loading routing table:', error); |
|
|
this.updateRoutingTable([]); |
|
|
} |
|
|
} |
|
|
|
|
|
updateRoutingTable(routes) { |
|
|
const tbody = document.getElementById('routesTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
routes.forEach(route => { |
|
|
const row = document.createElement('tr'); |
|
|
row.innerHTML = ` |
|
|
<td>${route.destination}</td> |
|
|
<td>${route.gateway || 'Direct'}</td> |
|
|
<td>${route.interface}</td> |
|
|
<td>${route.metric}</td> |
|
|
<td>${route.type}</td> |
|
|
<td>${route.use_count || 0}</td> |
|
|
<td>${route.last_used ? new Date(route.last_used * 1000).toLocaleString() : 'Never'}</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
async loadInterfaces() { |
|
|
try { |
|
|
const response = await this.apiCall('/router/interfaces'); |
|
|
this.updateInterfacesTable(response.interfaces); |
|
|
} catch (error) { |
|
|
console.error('Error loading interfaces:', error); |
|
|
this.updateInterfacesTable([]); |
|
|
} |
|
|
} |
|
|
|
|
|
updateInterfacesTable(interfaces) { |
|
|
const tbody = document.getElementById('interfacesTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
interfaces.forEach(iface => { |
|
|
const row = document.createElement('tr'); |
|
|
row.innerHTML = ` |
|
|
<td>${iface.name}</td> |
|
|
<td>${iface.ip_address}</td> |
|
|
<td>${iface.network}</td> |
|
|
<td>${iface.mtu}</td> |
|
|
<td><span class="status-badge status-${iface.enabled ? 'active' : 'inactive'}">${iface.enabled ? 'Up' : 'Down'}</span></td> |
|
|
<td> |
|
|
<button class="btn btn-secondary btn-sm"> |
|
|
<i class="fas fa-cog"></i> Configure |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
async loadARPTable() { |
|
|
try { |
|
|
const response = await this.apiCall('/router/arp'); |
|
|
this.updateARPTable(response.arp_table); |
|
|
} catch (error) { |
|
|
console.error('Error loading ARP table:', error); |
|
|
this.updateARPTable([]); |
|
|
} |
|
|
} |
|
|
|
|
|
updateARPTable(arpEntries) { |
|
|
const tbody = document.getElementById('arpTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
arpEntries.forEach(entry => { |
|
|
const row = document.createElement('tr'); |
|
|
row.innerHTML = ` |
|
|
<td>${entry.ip_address}</td> |
|
|
<td>${entry.mac_address}</td> |
|
|
<td> |
|
|
<button class="btn btn-danger btn-sm"> |
|
|
<i class="fas fa-trash"></i> Clear |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async loadBridgeData() { |
|
|
try { |
|
|
const [clientsResponse, statsResponse] = await Promise.all([ |
|
|
this.apiCall('/bridge/clients'), |
|
|
this.apiCall('/bridge/stats') |
|
|
]); |
|
|
|
|
|
this.updateBridgeTable(clientsResponse.clients); |
|
|
} catch (error) { |
|
|
console.error('Error loading bridge data:', error); |
|
|
this.updateBridgeTable({}); |
|
|
} |
|
|
} |
|
|
|
|
|
updateBridgeTable(clients) { |
|
|
const tbody = document.getElementById('bridgeTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
Object.values(clients).forEach(client => { |
|
|
const row = document.createElement('tr'); |
|
|
row.innerHTML = ` |
|
|
<td>${client.client_id}</td> |
|
|
<td>${client.bridge_type}</td> |
|
|
<td>${client.remote_address}:${client.remote_port}</td> |
|
|
<td>${new Date(client.connected_time * 1000).toLocaleString()}</td> |
|
|
<td>${client.packets_received} / ${client.packets_sent}</td> |
|
|
<td>${this.formatBytes(client.bytes_received)} / ${this.formatBytes(client.bytes_sent)}</td> |
|
|
<td> |
|
|
<button class="btn btn-danger btn-sm" onclick="app.disconnectBridgeClient('${client.client_id}')"> |
|
|
<i class="fas fa-times"></i> Disconnect |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async loadSessionsData() { |
|
|
try { |
|
|
const [sessionsResponse, summaryResponse] = await Promise.all([ |
|
|
this.apiCall('/sessions?limit=100'), |
|
|
this.apiCall('/sessions/summary') |
|
|
]); |
|
|
|
|
|
this.updateSessionSummary(summaryResponse.summary); |
|
|
this.updateSessionsTable(sessionsResponse.sessions); |
|
|
} catch (error) { |
|
|
console.error('Error loading sessions data:', error); |
|
|
this.updateSessionSummary({}); |
|
|
this.updateSessionsTable([]); |
|
|
} |
|
|
} |
|
|
|
|
|
updateSessionSummary(summary) { |
|
|
const container = document.getElementById('sessionSummary'); |
|
|
container.innerHTML = ` |
|
|
<h3>Session Summary</h3> |
|
|
<div class="stat-row"> |
|
|
<div class="stat-item"> |
|
|
<span class="stat-label">Total Sessions</span> |
|
|
<span class="stat-value">${summary.total_sessions || 0}</span> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<span class="stat-label">Active (Last Hour)</span> |
|
|
<span class="stat-value">${summary.active_sessions_by_age?.last_hour || 0}</span> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<span class="stat-label">Active (Last Day)</span> |
|
|
<span class="stat-value">${summary.active_sessions_by_age?.last_day || 0}</span> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
updateSessionsTable(sessions) { |
|
|
const tbody = document.getElementById('sessionsTableBody'); |
|
|
tbody.innerHTML = ''; |
|
|
|
|
|
sessions.forEach(session => { |
|
|
const row = document.createElement('tr'); |
|
|
row.innerHTML = ` |
|
|
<td>${session.session_id.substring(0, 8)}...</td> |
|
|
<td>${session.session_type}</td> |
|
|
<td><span class="status-badge status-${session.state.toLowerCase()}">${session.state}</span></td> |
|
|
<td>${session.virtual_ip || '-'}${session.virtual_port ? ':' + session.virtual_port : ''}</td> |
|
|
<td>${session.real_ip || '-'}${session.real_port ? ':' + session.real_port : ''}</td> |
|
|
<td>${session.protocol || '-'}</td> |
|
|
<td>${this.formatDuration(session.duration)}</td> |
|
|
<td>${this.formatDuration(session.idle_time)}</td> |
|
|
<td>${this.formatBytes(session.metrics.total_bytes)}</td> |
|
|
`; |
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async loadLogsData() { |
|
|
try { |
|
|
const response = await this.apiCall('/logs?limit=100'); |
|
|
this.updateLogsContainer(response.logs); |
|
|
} catch (error) { |
|
|
console.error('Error loading logs data:', error); |
|
|
this.updateLogsContainer([]); |
|
|
} |
|
|
} |
|
|
|
|
|
updateLogsContainer(logs) { |
|
|
const container = document.getElementById('logContainer'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
logs.forEach(log => { |
|
|
const entry = document.createElement('div'); |
|
|
entry.className = 'log-entry'; |
|
|
entry.innerHTML = ` |
|
|
<div class="log-level ${log.level}">${log.level}</div> |
|
|
<div class="log-content"> |
|
|
<div class="log-timestamp">${new Date(log.timestamp * 1000).toLocaleString()}</div> |
|
|
<div class="log-message">${log.message}</div> |
|
|
${log.metadata && Object.keys(log.metadata).length > 0 ? |
|
|
`<div class="log-metadata">${JSON.stringify(log.metadata)}</div>` : ''} |
|
|
</div> |
|
|
`; |
|
|
container.appendChild(entry); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async loadConfiguration() { |
|
|
try { |
|
|
const response = await this.apiCall('/config'); |
|
|
this.updateConfigurationForms(response.config); |
|
|
} catch (error) { |
|
|
console.error('Error loading configuration:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
updateConfigurationForms(config) { |
|
|
|
|
|
const dhcpConfig = document.getElementById('dhcpConfig'); |
|
|
dhcpConfig.innerHTML = ` |
|
|
<div class="form-group"> |
|
|
<label>Network:</label> |
|
|
<input type="text" value="${config.dhcp?.network || ''}" name="dhcp_network"> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label>Range Start:</label> |
|
|
<input type="text" value="${config.dhcp?.range_start || ''}" name="dhcp_range_start"> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label>Range End:</label> |
|
|
<input type="text" value="${config.dhcp?.range_end || ''}" name="dhcp_range_end"> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label>Lease Time (seconds):</label> |
|
|
<input type="number" value="${config.dhcp?.lease_time || 3600}" name="dhcp_lease_time"> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
|
|
|
const natConfig = document.getElementById('natConfig'); |
|
|
natConfig.innerHTML = ` |
|
|
<div class="form-group"> |
|
|
<label>Port Range Start:</label> |
|
|
<input type="number" value="${config.nat?.port_range_start || 10000}" name="nat_port_start"> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label>Port Range End:</label> |
|
|
<input type="number" value="${config.nat?.port_range_end || 65535}" name="nat_port_end"> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label>Session Timeout (seconds):</label> |
|
|
<input type="number" value="${config.nat?.session_timeout || 300}" name="nat_timeout"> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
|
|
|
const firewallConfig = document.getElementById('firewallConfig'); |
|
|
firewallConfig.innerHTML = ` |
|
|
<div class="form-group"> |
|
|
<label>Default Policy:</label> |
|
|
<select name="firewall_default_policy"> |
|
|
<option value="ACCEPT" ${config.firewall?.default_policy === 'ACCEPT' ? 'selected' : ''}>Accept</option> |
|
|
<option value="DROP" ${config.firewall?.default_policy === 'DROP' ? 'selected' : ''}>Drop</option> |
|
|
</select> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label>Log Blocked:</label> |
|
|
<select name="firewall_log_blocked"> |
|
|
<option value="true" ${config.firewall?.log_blocked ? 'selected' : ''}>Yes</option> |
|
|
<option value="false" ${!config.firewall?.log_blocked ? 'selected' : ''}>No</option> |
|
|
</select> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
setupCharts() { |
|
|
|
|
|
const trafficCtx = document.getElementById('trafficChart'); |
|
|
if (trafficCtx) { |
|
|
this.charts.traffic = new Chart(trafficCtx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: [], |
|
|
datasets: [{ |
|
|
label: 'Bytes In', |
|
|
data: [], |
|
|
borderColor: '#4facfe', |
|
|
backgroundColor: 'rgba(79, 172, 254, 0.1)', |
|
|
tension: 0.4 |
|
|
}, { |
|
|
label: 'Bytes Out', |
|
|
data: [], |
|
|
borderColor: '#00f2fe', |
|
|
backgroundColor: 'rgba(0, 242, 254, 0.1)', |
|
|
tension: 0.4 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
scales: { |
|
|
y: { |
|
|
beginAtZero: true |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const connectionCtx = document.getElementById('connectionChart'); |
|
|
if (connectionCtx) { |
|
|
this.charts.connection = new Chart(connectionCtx, { |
|
|
type: 'doughnut', |
|
|
data: { |
|
|
labels: ['DHCP', 'NAT', 'TCP', 'Bridge'], |
|
|
datasets: [{ |
|
|
data: [0, 0, 0, 0], |
|
|
backgroundColor: [ |
|
|
'#4facfe', |
|
|
'#00f2fe', |
|
|
'#51cf66', |
|
|
'#ff6b6b' |
|
|
] |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
updateCharts(stats) { |
|
|
|
|
|
if (this.charts.traffic) { |
|
|
const now = new Date(); |
|
|
const labels = this.charts.traffic.data.labels; |
|
|
const bytesIn = this.charts.traffic.data.datasets[0].data; |
|
|
const bytesOut = this.charts.traffic.data.datasets[1].data; |
|
|
|
|
|
labels.push(now.toLocaleTimeString()); |
|
|
bytesIn.push(Math.random() * 1000000); |
|
|
bytesOut.push(Math.random() * 800000); |
|
|
|
|
|
|
|
|
if (labels.length > 10) { |
|
|
labels.shift(); |
|
|
bytesIn.shift(); |
|
|
bytesOut.shift(); |
|
|
} |
|
|
|
|
|
this.charts.traffic.update(); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.charts.connection && stats) { |
|
|
this.charts.connection.data.datasets[0].data = [ |
|
|
Object.keys(stats.dhcp || {}).length, |
|
|
stats.nat?.active_sessions || 0, |
|
|
Object.keys(stats.tcp || {}).length, |
|
|
stats.bridge?.active_clients || 0 |
|
|
]; |
|
|
this.charts.connection.update(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
showModal(modalId) { |
|
|
document.getElementById(modalId).style.display = 'block'; |
|
|
} |
|
|
|
|
|
closeModal(modalId) { |
|
|
document.getElementById(modalId).style.display = 'none'; |
|
|
} |
|
|
|
|
|
showAddRuleModal() { |
|
|
this.showModal('addRuleModal'); |
|
|
} |
|
|
|
|
|
async addFirewallRule() { |
|
|
try { |
|
|
const form = document.getElementById('addRuleForm'); |
|
|
const formData = new FormData(form); |
|
|
const ruleData = Object.fromEntries(formData.entries()); |
|
|
|
|
|
await this.apiCall('/firewall/rules', { |
|
|
method: 'POST', |
|
|
body: JSON.stringify(ruleData) |
|
|
}); |
|
|
|
|
|
this.showToast('Firewall rule added successfully', 'success'); |
|
|
this.closeModal('addRuleModal'); |
|
|
form.reset(); |
|
|
await this.loadFirewallData(); |
|
|
} catch (error) { |
|
|
console.error('Error adding firewall rule:', error); |
|
|
this.showToast('Error adding firewall rule', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async saveConfiguration() { |
|
|
try { |
|
|
const configData = this.collectConfigurationData(); |
|
|
await this.apiCall('/config', { |
|
|
method: 'POST', |
|
|
body: JSON.stringify(configData) |
|
|
}); |
|
|
|
|
|
this.showToast('Configuration saved successfully', 'success'); |
|
|
} catch (error) { |
|
|
console.error('Error saving configuration:', error); |
|
|
this.showToast('Error saving configuration', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
collectConfigurationData() { |
|
|
const config = {}; |
|
|
|
|
|
|
|
|
const dhcpInputs = document.querySelectorAll('#dhcpConfig input, #dhcpConfig select'); |
|
|
config.dhcp = {}; |
|
|
dhcpInputs.forEach(input => { |
|
|
const key = input.name.replace('dhcp_', ''); |
|
|
config.dhcp[key] = input.type === 'number' ? parseInt(input.value) : input.value; |
|
|
}); |
|
|
|
|
|
|
|
|
const natInputs = document.querySelectorAll('#natConfig input, #natConfig select'); |
|
|
config.nat = {}; |
|
|
natInputs.forEach(input => { |
|
|
const key = input.name.replace('nat_', ''); |
|
|
config.nat[key] = input.type === 'number' ? parseInt(input.value) : input.value; |
|
|
}); |
|
|
|
|
|
|
|
|
const firewallInputs = document.querySelectorAll('#firewallConfig input, #firewallConfig select'); |
|
|
config.firewall = {}; |
|
|
firewallInputs.forEach(input => { |
|
|
const key = input.name.replace('firewall_', ''); |
|
|
config.firewall[key] = input.type === 'checkbox' ? input.checked : input.value; |
|
|
}); |
|
|
|
|
|
return config; |
|
|
} |
|
|
|
|
|
|
|
|
formatBytes(bytes) { |
|
|
if (bytes === 0) return '0 B'; |
|
|
const k = 1024; |
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
|
} |
|
|
|
|
|
formatDuration(seconds) { |
|
|
if (seconds < 60) return `${Math.round(seconds)}s`; |
|
|
if (seconds < 3600) return `${Math.round(seconds / 60)}m`; |
|
|
if (seconds < 86400) return `${Math.round(seconds / 3600)}h`; |
|
|
return `${Math.round(seconds / 86400)}d`; |
|
|
} |
|
|
|
|
|
|
|
|
showLoading() { |
|
|
document.getElementById('loadingOverlay').style.display = 'block'; |
|
|
} |
|
|
|
|
|
hideLoading() { |
|
|
document.getElementById('loadingOverlay').style.display = 'none'; |
|
|
} |
|
|
|
|
|
|
|
|
showToast(message, type = 'info') { |
|
|
const container = document.getElementById('toastContainer'); |
|
|
const toast = document.createElement('div'); |
|
|
toast.className = `toast ${type}`; |
|
|
toast.textContent = message; |
|
|
|
|
|
container.appendChild(toast); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
toast.remove(); |
|
|
}, 5000); |
|
|
} |
|
|
|
|
|
|
|
|
startAutoRefresh() { |
|
|
this.refreshInterval = setInterval(async () => { |
|
|
if (this.currentSection === 'dashboard') { |
|
|
await this.loadSystemStatus(); |
|
|
await this.loadDashboardData(); |
|
|
} |
|
|
}, 30000); |
|
|
} |
|
|
|
|
|
stopAutoRefresh() { |
|
|
if (this.refreshInterval) { |
|
|
clearInterval(this.refreshInterval); |
|
|
this.refreshInterval = null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async refreshData() { |
|
|
this.showLoading(); |
|
|
try { |
|
|
await this.loadSectionData(this.currentSection); |
|
|
this.showToast('Data refreshed successfully', 'success'); |
|
|
} catch (error) { |
|
|
this.showToast('Error refreshing data', 'error'); |
|
|
} finally { |
|
|
this.hideLoading(); |
|
|
} |
|
|
} |
|
|
|
|
|
async refreshDHCPLeases() { |
|
|
await this.loadDHCPData(); |
|
|
this.showToast('DHCP leases refreshed', 'info'); |
|
|
} |
|
|
|
|
|
async refreshNATSessions() { |
|
|
await this.loadNATData(); |
|
|
this.showToast('NAT sessions refreshed', 'info'); |
|
|
} |
|
|
|
|
|
async refreshFirewallRules() { |
|
|
await this.loadFirewallData(); |
|
|
this.showToast('Firewall rules refreshed', 'info'); |
|
|
} |
|
|
|
|
|
async refreshBridgeClients() { |
|
|
await this.loadBridgeData(); |
|
|
this.showToast('Bridge clients refreshed', 'info'); |
|
|
} |
|
|
|
|
|
async refreshSessions() { |
|
|
await this.loadSessionsData(); |
|
|
this.showToast('Sessions refreshed', 'info'); |
|
|
} |
|
|
|
|
|
async refreshLogs() { |
|
|
await this.loadLogsData(); |
|
|
this.showToast('Logs refreshed', 'info'); |
|
|
} |
|
|
|
|
|
|
|
|
filterSessions() { |
|
|
|
|
|
const filter = document.getElementById('sessionTypeFilter').value; |
|
|
|
|
|
} |
|
|
|
|
|
filterLogs() { |
|
|
|
|
|
const levelFilter = document.getElementById('logLevelFilter').value; |
|
|
const categoryFilter = document.getElementById('logCategoryFilter').value; |
|
|
|
|
|
} |
|
|
|
|
|
searchLogs() { |
|
|
|
|
|
const searchTerm = document.getElementById('logSearchInput').value; |
|
|
|
|
|
} |
|
|
|
|
|
clearLogs() { |
|
|
if (confirm('Are you sure you want to clear all logs?')) { |
|
|
document.getElementById('logContainer').innerHTML = ''; |
|
|
this.showToast('Logs cleared', 'info'); |
|
|
} |
|
|
} |
|
|
|
|
|
resetConfiguration() { |
|
|
if (confirm('Are you sure you want to reset configuration to defaults?')) { |
|
|
this.loadConfiguration(); |
|
|
this.showToast('Configuration reset to defaults', 'info'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let app; |
|
|
|
|
|
function refreshData() { |
|
|
app.refreshData(); |
|
|
} |
|
|
|
|
|
function showAddRuleModal() { |
|
|
app.showAddRuleModal(); |
|
|
} |
|
|
|
|
|
function closeModal(modalId) { |
|
|
app.closeModal(modalId); |
|
|
} |
|
|
|
|
|
function addFirewallRule() { |
|
|
app.addFirewallRule(); |
|
|
} |
|
|
|
|
|
function saveConfiguration() { |
|
|
app.saveConfiguration(); |
|
|
} |
|
|
|
|
|
function resetConfiguration() { |
|
|
app.resetConfiguration(); |
|
|
} |
|
|
|
|
|
function refreshDHCPLeases() { |
|
|
app.refreshDHCPLeases(); |
|
|
} |
|
|
|
|
|
function refreshNATSessions() { |
|
|
app.refreshNATSessions(); |
|
|
} |
|
|
|
|
|
function refreshFirewallRules() { |
|
|
app.refreshFirewallRules(); |
|
|
} |
|
|
|
|
|
function refreshBridgeClients() { |
|
|
app.refreshBridgeClients(); |
|
|
} |
|
|
|
|
|
function refreshSessions() { |
|
|
app.refreshSessions(); |
|
|
} |
|
|
|
|
|
function refreshLogs() { |
|
|
app.refreshLogs(); |
|
|
} |
|
|
|
|
|
function filterSessions() { |
|
|
app.filterSessions(); |
|
|
} |
|
|
|
|
|
function filterLogs() { |
|
|
app.filterLogs(); |
|
|
} |
|
|
|
|
|
function searchLogs() { |
|
|
app.searchLogs(); |
|
|
} |
|
|
|
|
|
function clearLogs() { |
|
|
app.clearLogs(); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
app = new VirtualISPApp(); |
|
|
}); |
|
|
|
|
|
|