Excluding Cross‑Property Devices from Metrics and Adding Live Scan Tracking in monitor‑parches‑uhpcm
Excluding Cross‑Property Devices from Metrics and Adding Live Scan Tracking in monitor‑parches‑uhpcm TL;DR: I refactored the analysis layer to drop equipment belonging to other properties (AHCUN, BKCUN, …) and exposed
Excluding Cross‑Property Devices from Metrics and Adding Live Scan Tracking in monitor‑parches‑uhpcm
TL;DR: I refactored the analysis layer to drop equipment belonging to other properties (AHCUN, BKCUN, …) and exposed a live “scan launched” flag in the UI. The change removes noisy data from the dashboards and gives operators instant feedback when a scan is triggered from Endpoint Central.
The Problem
Our internal patch‑monitoring dashboard aggregates device data from several physical locations (Playa, AHCUN, BKCUN, KKCUN, HRPUJ, …). The metrics view (/metrics) displayed totals for all devices, even though the user had selected a single property. The result was inflated “missing patches” counts and misleading charts.
On top of that, when a technician forced a scan from the “Force Scan” pop‑up, the UI never reflected that the scan had been queued. Users kept clicking “Force Scan” because there was no visual cue that the request was already in flight. The symptom was a UI that looked static while the backend was already processing the request.
Both issues boiled down to two things:
- Data leakage – filtering was only happening at the presentation layer, after the heavy analysis had already run.
-
Missing state propagation – the scan status lived only in the backend (
service.mjs) and never made it back to the front‑end in real time.
What I Tried First
My first attempt was to keep the existing analysis code untouched and filter the results in public/app.js right before rendering the table:
// naive UI‑side filter (failed)
const filtered = data.devices.filter(d => d.property === currentProperty);
renderMetrics(filtered);
The UI filtered correctly, but the chart calculations (summarize, groupsFor) still ran on the full data set, so the numbers were still wrong. Moreover, the filter added noticeable lag because we were processing thousands of rows twice.
For the scan‑status problem I added a simple setTimeout after the POST request to /api/scan that toggled a CSS class, hoping the delay would be enough to show the “in‑flight” icon. That hack was flaky – network latency varied and the UI sometimes showed the scan as completed before the backend even started.
Both approaches failed because they treated symptoms at the wrong layer.
The Implementation
1. Filter at the analysis layer
lib/analysis.mjs holds the core configuration (DEFAULT_CONFIG) and the functions that turn raw device reports into grouped metrics. I added a property filter directly into the config so that all downstream functions automatically ignore out‑of‑scope devices.
@@ -6,6 +6,10 @@ export const DEFAULT_CONFIG = {
{ id: 'net176', name: 'Segmento 10.176', rules: [{ field: 'ip', pattern: '^10\\.176\\.' }] },
],
homeNetwork: '^10\\.176\\.',
+// Property filter – only include devices that belong to the selected property
+propertyWhitelist: ['PLAYA', 'AHCUN', 'BKCUN', 'KKCUN', 'HRPUJ'],
+isPropertyAllowed: (device) => DEFAULT_CONFIG.propertyWhitelist.includes(device.property),
};
Next, I updated the main analyze function to short‑circuit devices that fail the whitelist test:
@@ -42,7 +45,12 @@ export function analyze(report, opts = {}) {
const groups = {};
for (const device of report.devices) {
- // existing grouping logic …
+ // NEW: drop devices from other properties early
+ if (!DEFAULT_CONFIG.isPropertyAllowed(device)) {
+ continue; // skip this device completely
+ }
+
+ // existing grouping logic …
const key = getGroupKey(device);
if (!groups[key]) groups[key] = [];
groups[key].push(device);
Because the filter runs before any heavy grouping, the downstream summarize and groupsFor functions now operate on a clean data set, fixing the inflated totals without any extra UI‑side work.
2. Propagate scan status in real time
The scan request is handled in lib/service.mjs. I added a small in‑memory map (activeScans) that records which device IDs have a pending scan. The map is exposed via a new endpoint /api/scan/status.
@@ -300,6 +300,23 @@ export function createService(store, opts = {}) {
const activeScans = new Map(); // <deviceId, { by: user, ts: Date }>
async function launchScan(deviceId, user) {
+ // Mark the scan as active before calling Endpoint Central
+ activeScans.set(deviceId, { by: user, ts: new Date() });
+ try {
+ await endpointCentral.triggerScan(deviceId);
+ } finally {
+ // The scan will be cleared by the periodic refresh (see below)
+ }
}
// New endpoint – returns JSON of active scans
app.get('/api/scan/status', (req, res) => {
- res.json({}); // placeholder
+ res.json(Object.fromEntries(activeScans));
});
On the front‑end (public/app.js) I added a poller that fetches /api/scan/status every 5 seconds and updates the UI accordingly:
// poll active scans
setInterval(async () => {
const resp = await fetch('/api/scan/status');
const active = await resp.json(); // { deviceId: {by, ts} }
updateScanFlags(active);
}, 5000);
function updateScanFlags(active) {
document.querySelectorAll('.device-row').forEach(row => {
const id = row.dataset.id;
if (active[id]) {
row.querySelector('.scanok').classList.add('active');
row.querySelector('.scanok').title = `Escaneo lanzado por ${active[id].by}`;
} else {
row.querySelector('.scanok').classList.remove('active');
}
});
}
The HTML snippet for the scan flag now includes a title attribute that shows the user who triggered the scan:
@@ -138,7 +138,13 @@ function trackingHTML() {
if (sc) return ` · <span class="scanok" title="Escaneo lanzado en Endpoint Central por ${esc(sc.by)}; se confirmará con el próximo PDF">↻ escaneo la`;
return '';
}
3. UI polish – final loading bubble
While working on the scan status, I realized the page lacked a clear “everything is done” indicator. I added a toast‑style bubble that appears when the last pending scan finishes.
diff
@@ -260,12 +264,16 @@ function showTip(html, ev) {
const hide
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/monitor-parches-uhpcm` · 2026-10-13*
\#playadev #buildinpublic
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.