174 lines
8.7 KiB
HTML
174 lines
8.7 KiB
HTML
<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<title>Network Audit Tracker</title>
|
||
|
|
<link rel="icon" href="habiwan.png" type="image/png">
|
||
|
|
<style>
|
||
|
|
body { font-family: system-ui, sans-serif; background-color: #1e1e2e; color: #cdd6f4; max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
|
||
|
|
h2 { border-bottom: 2px solid #313244; padding-bottom: 0.5rem; }
|
||
|
|
.controls { margin-top: 1rem; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||
|
|
.form-group { display: flex; gap: 10px; margin-bottom: 1.5rem; flex-wrap: wrap; }
|
||
|
|
input[type="text"], select, button { padding: 8px 12px; background: #313244; border: 1px solid #45475a; color: #cdd6f4; border-radius: 4px; }
|
||
|
|
input.edit-input { width: 90%; }
|
||
|
|
button { font-weight: bold; cursor: pointer; border: none; transition: 0.2s; }
|
||
|
|
button:hover { opacity: 0.8; }
|
||
|
|
button.action-btn { background: #89b4fa; color: #11111b; }
|
||
|
|
button.delete-btn { background: #f38ba8; color: #11111b; }
|
||
|
|
button.export-btn { background: #a6e3a1; color: #11111b; }
|
||
|
|
button.import-btn { background: #f9e2af; color: #11111b; }
|
||
|
|
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
||
|
|
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #313244; }
|
||
|
|
th { cursor: pointer; user-select: none; }
|
||
|
|
/* Disable cursor for the non-sortable # column */
|
||
|
|
th.static { cursor: default; }
|
||
|
|
.badge { padding: 4px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: bold; }
|
||
|
|
.type-infrastructure { background: #f38ba8; color: #11111b; }
|
||
|
|
.type-service { background: #fab387; color: #11111b; }
|
||
|
|
.type-client { background: #a6e3a1; color: #11111b; }
|
||
|
|
.type-transient { background: #cba6f7; color: #11111b; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<h1>Network Audit Tracker</h1>
|
||
|
|
<p>Device Count: <span id="deviceCount">0</span></p>
|
||
|
|
|
||
|
|
<form class="form-group" id="addForm">
|
||
|
|
<input type="text" id="hostname" placeholder="Hostname" required>
|
||
|
|
<input type="text" id="ip" placeholder="IP" required>
|
||
|
|
<input type="text" id="mac" placeholder="MAC Address" required>
|
||
|
|
<select id="type">
|
||
|
|
<option value="Infrastructure">Infrastructure</option>
|
||
|
|
<option value="Service">Service</option>
|
||
|
|
<option value="Client">Client</option>
|
||
|
|
<option value="Transient">Transient</option>
|
||
|
|
</select>
|
||
|
|
<button type="submit" class="action-btn">Add Device</button>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
|
||
|
|
<th class="static">#</th>
|
||
|
|
<th onclick="sortData('hostname')">Hostname ↕️</th>
|
||
|
|
<th onclick="sortData('ip')">IP Address ↕️</th>
|
||
|
|
<th onclick="sortData('mac')">MAC Address ↕️</th>
|
||
|
|
<th onclick="sortData('type')">Type ↕️</th>
|
||
|
|
<th>Action</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="tableBody"></tbody>
|
||
|
|
</table>
|
||
|
|
|
||
|
|
<div class="controls">
|
||
|
|
<button class="export-btn" onclick="exportToCSV()">Export CSV</button>
|
||
|
|
<button class="import-btn" onclick="document.getElementById('importFile').click()">Import CSV</button>
|
||
|
|
<button class="delete-btn" onclick="deleteSelected()">Delete Selected</button>
|
||
|
|
<input type="file" id="importFile" accept=".csv" style="display:none" onchange="importCSV(event)">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
let devices = JSON.parse(localStorage.getItem('networkAuditData')) || [];
|
||
|
|
let sortAscending = true;
|
||
|
|
let currentSortColumn = '';
|
||
|
|
const tableBody = document.getElementById('tableBody');
|
||
|
|
const deviceCount = document.getElementById('deviceCount');
|
||
|
|
|
||
|
|
function ipToNumber(ip) {
|
||
|
|
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderTable() {
|
||
|
|
tableBody.innerHTML = '';
|
||
|
|
devices.forEach((device, index) => {
|
||
|
|
const tr = document.createElement('tr');
|
||
|
|
const isEditing = device.isEditing;
|
||
|
|
|
||
|
|
tr.innerHTML = `
|
||
|
|
<td><input type="checkbox" ${device.selected ? 'checked' : ''} onchange="toggleSelect(${device.id})"></td>
|
||
|
|
<td>${index + 1}</td>
|
||
|
|
<td>${isEditing ? `<input type="text" class="edit-input" value="${device.hostname}" onchange="updateField(${device.id}, 'hostname', this.value)">` : device.hostname}</td>
|
||
|
|
<td>${isEditing ? `<input type="text" class="edit-input" value="${device.ip}" onchange="updateField(${device.id}, 'ip', this.value)">` : device.ip}</td>
|
||
|
|
<td>${isEditing ? `<input type="text" class="edit-input" value="${device.mac}" onchange="updateField(${device.id}, 'mac', this.value)">` : device.mac}</td>
|
||
|
|
<td>${isEditing ? `<select onchange="updateField(${device.id}, 'type', this.value)"><option value="Infrastructure" ${device.type==='Infrastructure'?'selected':''}>Infrastructure</option><option value="Service" ${device.type==='Service'?'selected':''}>Service</option><option value="Client" ${device.type==='Client'?'selected':''}>Client</option><option value="Transient" ${device.type==='Transient'?'selected':''}>Transient</option></select>` : `<span class="badge type-${device.type.toLowerCase()}">${device.type}</span>`}</td>
|
||
|
|
<td><button class="action-btn" onclick="toggleEdit(${device.id})">${isEditing ? '💾' : '✏️'}</button></td>
|
||
|
|
`;
|
||
|
|
tableBody.appendChild(tr);
|
||
|
|
});
|
||
|
|
deviceCount.innerText = devices.length;
|
||
|
|
localStorage.setItem('networkAuditData', JSON.stringify(devices));
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleEdit(id) {
|
||
|
|
const dev = devices.find(d => d.id === id);
|
||
|
|
dev.isEditing = !dev.isEditing;
|
||
|
|
renderTable();
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateField(id, field, value) {
|
||
|
|
const dev = devices.find(d => d.id === id);
|
||
|
|
dev[field] = value;
|
||
|
|
localStorage.setItem('networkAuditData', JSON.stringify(devices));
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleSelect(id) {
|
||
|
|
const dev = devices.find(d => d.id === id);
|
||
|
|
dev.selected = !dev.selected;
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleSelectAll() {
|
||
|
|
const allChecked = document.getElementById('selectAll').checked;
|
||
|
|
devices.forEach(d => d.selected = allChecked);
|
||
|
|
renderTable();
|
||
|
|
}
|
||
|
|
|
||
|
|
function deleteSelected() {
|
||
|
|
devices = devices.filter(d => !d.selected);
|
||
|
|
renderTable();
|
||
|
|
}
|
||
|
|
|
||
|
|
function sortData(key) {
|
||
|
|
if (currentSortColumn === key) sortAscending = !sortAscending;
|
||
|
|
else { currentSortColumn = key; sortAscending = true; }
|
||
|
|
devices.sort((a, b) => {
|
||
|
|
let valA = key === 'ip' ? ipToNumber(a[key]) : a[key].toLowerCase();
|
||
|
|
let valB = key === 'ip' ? ipToNumber(b[key]) : b[key].toLowerCase();
|
||
|
|
return sortAscending ? (valA < valB ? -1 : 1) : (valA > valB ? -1 : 1);
|
||
|
|
});
|
||
|
|
renderTable();
|
||
|
|
}
|
||
|
|
|
||
|
|
function exportToCSV() {
|
||
|
|
let csvContent = "data:text/csv;charset=utf-8,Hostname,IP,MAC,Type\n" + devices.map(d => `${d.hostname},${d.ip},${d.mac},${d.type}`).join("\n");
|
||
|
|
const link = document.createElement("a");
|
||
|
|
link.setAttribute("href", encodeURI(csvContent));
|
||
|
|
link.setAttribute("download", "network_audit.csv");
|
||
|
|
link.click();
|
||
|
|
}
|
||
|
|
|
||
|
|
function importCSV(event) {
|
||
|
|
const file = event.target.files[0];
|
||
|
|
const reader = new FileReader();
|
||
|
|
reader.onload = (e) => {
|
||
|
|
e.target.result.split('\n').slice(1).forEach(line => {
|
||
|
|
const [hostname, ip, mac, type] = line.split(',');
|
||
|
|
if (hostname) devices.push({ id: Date.now() + Math.random(), hostname: hostname.trim(), ip: ip.trim(), mac: mac.trim(), type: type.trim(), isEditing: false, selected: false });
|
||
|
|
});
|
||
|
|
renderTable();
|
||
|
|
};
|
||
|
|
reader.readAsText(file);
|
||
|
|
}
|
||
|
|
|
||
|
|
document.getElementById('addForm').addEventListener('submit', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
devices.push({ id: Date.now(), hostname: document.getElementById('hostname').value, ip: document.getElementById('ip').value, mac: document.getElementById('mac').value.toUpperCase(), type: document.getElementById('type').value, isEditing: false, selected: false });
|
||
|
|
renderTable();
|
||
|
|
document.getElementById('addForm').reset();
|
||
|
|
});
|
||
|
|
|
||
|
|
renderTable();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|