Full health check: fix startup crash, add subnet input to network discovery

Server fixes:
- database.js: auto-create data/ directory on startup — was crashing on
  fresh clone because the gitignored directory didn't exist
- better-sqlite3 upgraded to latest (13.x) for Node 26 compatibility

Network discovery:
- discoverNetwork() accepts optional subnet param; scans that range instead
  of auto-detecting when provided
- New GET /api/network-subnets endpoint returns detected local interfaces
  so UI can offer quick-select buttons
- Discovery modal: subnet text input + Scan button + auto-detect quick-select;
  local subnet buttons populate once the modal is open
- server/index.js: pass subnet from request body to discoverNetwork()

Infra:
- React client runs on PORT=3456 in dev to avoid port conflicts
- .claude/launch.json added for one-command preview via Express (serves
  the pre-built client, no port conflict with the preview tool)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-06 17:21:34 -04:00
1 parent 59078362da
commit f119168399
14 files changed
+119 -427

No files matched your search

+5 -1
View File
@@ -1,7 +1,11 @@
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const db = new Database(path.join(__dirname, '../data/monitor.db'));
const dataDir = path.join(__dirname, '../data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
const db = new Database(path.join(dataDir, 'monitor.db'));
// Create tables
db.exec(`
+12 -2
View File
@@ -13,7 +13,7 @@ const notificationService = require('./notificationService');
const cache = require('./cache');
const security = require('./securityMiddleware');
const metrics = require('./metrics');
const { discoverNetwork } = require('./networkDiscovery');
const { discoverNetwork, getLocalSubnets } = require('./networkDiscovery');
const auditLog = require('./auditLog');
// Initialize notifications table if it doesn't exist
@@ -1541,7 +1541,8 @@ app.post('/api/test-ntfy', async (req, res) => {
// Network discovery — scan local network for monitorable hosts
app.post('/api/network-discovery', async (req, res) => {
try {
const hosts = await discoverNetwork();
const { subnet } = req.body || {};
const hosts = await discoverNetwork(subnet || null);
res.json({ hosts });
} catch (error) {
console.error('Network discovery error:', error);
@@ -1549,6 +1550,15 @@ app.post('/api/network-discovery', async (req, res) => {
}
});
// Return detected local subnets so the UI can offer them as quick-select options
app.get('/api/network-subnets', (req, res) => {
try {
res.json({ subnets: getLocalSubnets() });
} catch (e) {
res.json({ subnets: [] });
}
});
// Clear cache endpoint for debugging
app.post('/api/cache/clear', (req, res) => {
cache.clear();
+18 -9
View File
@@ -101,20 +101,29 @@ async function getArpHosts() {
});
}
async function discoverNetwork() {
const subnets = getLocalSubnets();
async function discoverNetwork(customSubnet = null) {
const arpHosts = await getArpHosts();
const arpIps = new Set(arpHosts.map(h => h.ip));
// Collect all IPs to scan: ARP table + subnet range
const allIps = new Map();
for (const h of arpHosts) allIps.set(h.ip, { ip: h.ip, hostname: h.hostname });
for (const subnet of subnets) {
if (!subnet.prefix) continue;
const range = subnetToRange(subnet.prefix);
for (const ip of range) {
if (!allIps.has(ip)) allIps.set(ip, { ip, hostname: null });
if (customSubnet) {
// Scan the user-specified subnet only (no ARP seeding, still probe all IPs in range)
const range = subnetToRange(customSubnet);
for (const ip of range) allIps.set(ip, { ip, hostname: null });
// Supplement with ARP hostnames for any IPs that happen to be in the ARP table
for (const h of arpHosts) {
if (allIps.has(h.ip) && h.hostname) allIps.get(h.ip).hostname = h.hostname;
}
} else {
// Auto-detect: ARP table + all local subnets
for (const h of arpHosts) allIps.set(h.ip, { ip: h.ip, hostname: h.hostname });
for (const subnet of getLocalSubnets()) {
if (!subnet.prefix) continue;
const range = subnetToRange(subnet.prefix);
for (const ip of range) {
if (!allIps.has(ip)) allIps.set(ip, { ip, hostname: null });
}
}
}