feat: Added Virtual Scrolling. This allows us to scan a whole /16 subnet without overloading the UI or using a lot of RAM.

This commit is contained in:
2026-09-24 09:43:55 -04:00
parent d84902c111
commit adc061ae26
3 changed files with 104 additions and 24 deletions
+1
View File
@@ -3,4 +3,5 @@
### 🚀 Features ### 🚀 Features
- Added changelog - Added changelog
- Added Themes
## [0.1.0] - 2026-09-23 ## [0.1.0] - 2026-09-23
+91 -24
View File
@@ -19,7 +19,7 @@ var theme = (() => {
} }
try { try {
localStorage.setItem(STORAGE_KEY, name); localStorage.setItem(STORAGE_KEY, name);
} catch (e) {} } catch (e) { }
} }
function init() { function init() {
@@ -28,7 +28,7 @@ var theme = (() => {
var picker = document.getElementById('theme-picker'); var picker = document.getElementById('theme-picker');
picker.value = saved; picker.value = saved;
picker.addEventListener('change', function() { picker.addEventListener('change', function () {
set(picker.value); set(picker.value);
}); });
} }
@@ -39,12 +39,18 @@ var theme = (() => {
return { init }; return { init };
})(); })();
const table = (() => { const table = (() => {
var dirty = false;
var flushTimer = null;
var ipIndex = {};
let data = []; let data = [];
let filtered = []; let filtered = [];
let sortKey = 'ip'; let sortKey = 'ip';
let sortDir = 'asc'; let sortDir = 'asc';
let filterText = ''; let filterText = '';
let checkedIps = new Set(); let checkedIps = new Set();
var ROW_HEIGHT = 32;
var BUFFER_ROWS = 20;
var scrollContainer = null;
function ipToNum(ip) { function ipToNum(ip) {
const parts = String(ip).split('.'); const parts = String(ip).split('.');
@@ -193,20 +199,61 @@ const table = (() => {
} }
function render() { function render() {
const tbody = document.getElementById('results-body'); var tbody = document.getElementById('results-body');
const count = document.getElementById('host-count'); var count = document.getElementById('host-count');
if (!tbody) return; if (!tbody) return;
expandedIp = null; expandedIp = null;
const rows = filtered.map((host) => { if (!scrollContainer) {
const ip = host.ip; scrollContainer = document.getElementById('results-table-container');
const status = host.status || 'Unknown'; scrollContainer.addEventListener('scroll', onScroll);
const cls = 'status-' + status.toLowerCase(); }
const rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-';
const hostname = host.hostname || '-';
const isChecked = checkedIps.has(ip) ? 'checked' : ''; renderVisible();
return '<tr data-ip="' + ip + '">'
if (count) {
var up = data.filter(function (h) { return h.status === 'Up'; }).length;
count.textContent = up + ' up / ' + data.length + ' total';
}
if (columns && columns.apply) columns.apply();
}
function renderVisible() {
var tbody = document.getElementById('results-body');
if (!tbody || !scrollContainer) return;
var totalRows = filtered.length;
var viewHeight = scrollContainer.clientHeight;
var scrollTop = scrollContainer.scrollTop;
var headerHeight = 36;
var startIdx = Math.max(0, Math.floor((scrollTop - headerHeight) / ROW_HEIGHT) - BUFFER_ROWS);
var visibleCount = Math.ceil(viewHeight / ROW_HEIGHT) + (BUFFER_ROWS * 2);
var endIdx = Math.min(totalRows, startIdx + visibleCount);
var topSpace = startIdx * ROW_HEIGHT;
var bottomSpace = (totalRows - endIdx) * ROW_HEIGHT;
var colCount = document.querySelectorAll('#results-table th').length;
var html = '';
if (topSpace > 0) {
html += '<tr class="virtual-spacer"><td colspan="' + colCount + '" style="height:' + topSpace + 'px"></td></tr>';
}
for (var i = startIdx; i < endIdx; i++) {
var host = filtered[i];
var ip = host.ip;
var status = host.status || 'Unknown';
var cls = 'status-' + status.toLowerCase();
var rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-';
var hostname = host.hostname || '-';
var isChecked = checkedIps.has(ip) ? 'checked' : '';
html += '<tr data-ip="' + ip + '">'
+ '<td class="col-check"><input type="checkbox" class="row-check" ' + isChecked + ' /></td>' + '<td class="col-check"><input type="checkbox" class="row-check" ' + isChecked + ' /></td>'
+ '<td>' + ip + '</td>' + '<td>' + ip + '</td>'
+ '<td class="' + cls + '">' + status + '</td>' + '<td class="' + cls + '">' + status + '</td>'
@@ -214,24 +261,43 @@ const table = (() => {
+ '<td>' + hostname + '</td>' + '<td>' + hostname + '</td>'
+ '<td class="ports-cell">' + formatPorts(host) + '</td>' + '<td class="ports-cell">' + formatPorts(host) + '</td>'
+ '</tr>'; + '</tr>';
});
tbody.innerHTML = rows.join('');
if (count) {
const up = data.filter((h) => h.status === 'Up').length;
count.textContent = up + ' up / ' + data.length + ' total';
} }
if (columns && columns.apply) columns.apply();
if (bottomSpace > 0) {
html += '<tr class="virtual-spacer"><td colspan="' + colCount + '" style="height:' + bottomSpace + 'px"></td></tr>';
}
tbody.innerHTML = html;
}
var scrollTimeout = null;
function onScroll() {
if (scrollTimeout) return;
scrollTimeout = requestAnimationFrame(function () {
scrollTimeout = null;
renderVisible();
if (columns && columns.apply) columns.apply();
});
} }
function upsert(host) { function upsert(host) {
const idx = data.findIndex((h) => h.ip === host.ip); var idx = data.findIndex(function (h) { return h.ip === host.ip; });
if (idx >= 0) { if (idx >= 0) {
data[idx] = host; data[idx] = host;
} else { } else {
ipIndex[host.ip] = data.length;
data.push(host); data.push(host);
} }
dirty = true;
if (!flushTimer) {
flushTimer = setTimeout(flush, 200);
}
}
function flush() {
flushTimer = null;
if (!dirty) return;
dirty = false;
refresh(); refresh();
} }
@@ -239,6 +305,7 @@ const table = (() => {
data = []; data = [];
filtered = []; filtered = [];
checkedIps.clear(); checkedIps.clear();
ipIndex = {};
document.getElementById('check-all').checked = false; document.getElementById('check-all').checked = false;
refresh(); refresh();
} }
@@ -1061,7 +1128,7 @@ var updater = (() => {
return { init }; return { init };
})(); })();
var changelog = (() => { var changelog = (() => {
var modal = function() { return document.getElementById('changelog-modal'); }; var modal = function () { return document.getElementById('changelog-modal'); };
var loaded = false; var loaded = false;
async function open() { async function open() {
@@ -1085,11 +1152,11 @@ var changelog = (() => {
document.getElementById('btn-changelog').addEventListener('click', open); document.getElementById('btn-changelog').addEventListener('click', open);
document.getElementById('changelog-close').addEventListener('click', close); document.getElementById('changelog-close').addEventListener('click', close);
modal().addEventListener('click', function(e) { modal().addEventListener('click', function (e) {
if (e.target === modal()) close(); if (e.target === modal()) close();
}); });
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && modal().classList.contains('visible')) close(); if (e.key === 'Escape' && modal().classList.contains('visible')) close();
}); });
} }
+12
View File
@@ -409,6 +409,7 @@ body {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
height: 32px;
} }
#results-table tbody tr { #results-table tbody tr {
@@ -428,6 +429,15 @@ body {
box-shadow: inset 3px 0 0 var(--cyan); box-shadow: inset 3px 0 0 var(--cyan);
} }
.virtual-spacer {
border: none !important;
}
.virtual-spacer td {
padding: 0 !important;
border: none !important;
}
/* ===== Status Colors ===== */ /* ===== Status Colors ===== */
.status-up { .status-up {
@@ -1322,6 +1332,7 @@ body {
.changelog-content a:hover { .changelog-content a:hover {
text-decoration: underline; text-decoration: underline;
} }
.theme-picker { .theme-picker {
padding: 1px 6px; padding: 1px 6px;
font-size: 11px; font-size: 11px;
@@ -1345,6 +1356,7 @@ body {
background: var(--bg-secondary); background: var(--bg-secondary);
color: var(--text-primary); color: var(--text-primary);
} }
/* ===== Theme: Midnight ===== */ /* ===== Theme: Midnight ===== */
:root[data-theme="midnight"] { :root[data-theme="midnight"] {