Chronological log of changes to the St. Mary's IT Portal. Newest at the top. The /devlog route on the deployed site renders this file.
Status: Live in production. All four monitored locations now show real client/device data on the dashboard: Parish Center 140, New Church 9, History Church 12, Stmary Lab 5 (UCK G2 Plus-stmary lab, UXG Lite- stmary-lab, USW-48-PoE, U6+, G5 Dome — Nano HD correctly filtered because its cloud status is offline).
Two-iteration path to green: 1. 6f2881c shipped the cloud client + service + config + debug endpoint. API key worked (200s), but the response mapping was wrong — I assumed /ea/devices returned a flat array; it actually returns { data: [{ hostId, hostName, devices: [...] }] }. Result: dashboard showed 1 empty-fielded row per lab host-group instead of 5 devices. 2. 0a1fcd4 fixed the mapping — find the host-group by hostId, iterate its .devices array, use correct field names (d.ip not d.ipAddress), and filter to status === 'online'.
Location card status caveat: Stmary Lab card still shows "Not configured" at the top of the dashboard because there's no TCP-probeable target in Azure's reach — the lab isn't on the S2S mesh (that's the whole point of using the cloud API). Location card = TCP probe result; "All UDM Clients" section = UniFi presence. Two independent signals, and for cloud sites only the second one matters.
Why: The Stmary Lab site (UCK G2 Plus at 192.168.11.102 on its own Verizon Fios WAN) isn't on the S2S VPN mesh and its historical IPsec tunnel to History Church (OldHall-oStmaryLab) is Offline. Rather than extend the production VPN to include a test/lab environment, we monitor it via Ubiquiti's cloud API — no VPN dependency, no attack-surface bleed.
New abstraction: unifiStatusService.CONTROLLERS[] entries now carry a source field — 'local' (existing VPN-based flow) or 'cloud' (new, polls api.ui.com with UNIFI_UI_API_KEY). The dashboard/API endpoints don't care which source populated the presence map; they just render it.
Files:
src/services/unifiCloudClient.js (new) — low-level GET against api.ui.com with X-API-KEY auth. Handles the { httpStatusCode, data, traceId } wrapper Ubiquiti uses.
src/services/unifiStatusService.js — refactored refresh() to dispatch by source. New refreshCloudController() fetches hosts, matches by name (UCK G2 Plus-stmary lab), then fetches adopted devices for that host.
config/config.json — new stmary-lab location with cloudManaged: trueflag (informational — the source-of-truth is the controller stanza).
src/routes/api.js — GET /api/debug/unifi-cloud returns raw responses from /ea/hosts, /ea/sites, /ea/devices for troubleshooting.
src/views/dashboard.ejs — cloud sites get a different missing-cred banner naming UNIFI_UI_API_KEY instead of the local _USER/_PASS pair.
src/views/settings.ejs — UniFi Client Discovery description nowdocuments both local + cloud auth patterns.
Known limitation: Ubiquiti's Site Manager API exposes adopted network devices per host, but per-site DHCP client enumeration may not be in every account tier. If it's absent, the Stmary Lab "All UDM Clients" section shows the adopted devices (UXG Lite, USW-48-PoE, U6+, Nano HD, UCK G2 Plus) instead of the full DHCP client list. Still a net upgrade over having no visibility at all. The /api/debug/unifi-cloud endpoint lets us inspect the actual schema returned by our specific account.
Status: Live in production. Dashboard shows all three sites Online: Parish Center (139 clients), New Church (9), History Church (11 — mostly G5 cameras + UNVR-oldhall + a Resideo gateway on Visitors VLAN).
Key finding along the way — the S2S mesh doesn't route Azure traffic for free. Parish Center UDM was correctly configured (route-based IPsec tunnel to Azure + SD-WAN mesh to History Church + OSPF-learned route to 10.10.10.0/24), so outbound Azure→History Church worked as soon as Azure's LNG address space included 10.10.10.0/24. But the return path was broken: History Church UDM had no route back to Azure's 10.5.0.0/16 VNet, so response packets were dropped. Symptom: UniFi request timeout in the diagnostic banner.
Fix (permanent): Added static route on History Church UDM via Network → Routing (Policy Engine → Routes):
`` 10.5.0.0/16 Next Hop: 192.168.6.4 Interface: UDM Pro Max-o-stmary Static Metric: 1 ``
Also documented as SSH one-liner for reference: ip route add 10.5.0.0/16 via 192.168.6.4 dev wgsts1002
UI gotcha logged for future-self: UniFi Network 10.4.x hides the "Static Routes" section behind the "Policy Engine → Routes" icon in the sidebar (the one with the "New" badge). And in the Create dialog, the default tab is OSPF — which is greyed out because SD-WAN owns it. Must switch to the Static tab before the form becomes editable.
Bonus discoveries in the History Church client list:
10.10.10.250 (now added to config.json).restroom, kitchen, and Fairfax Station Rd lot.
Discovery: unifi.ui.com Site Manager shows six UniFi entries under this account. Mapping to physical locations:
| unifi.ui.com site | Hardware | Physical location | Portal status | |---|---|---|---| | UDM Pro Max-o-stmary | UDM Pro Max | Parish Center | ✅ live | | UniFi-CloudKey-Gen2 | UCK G2 | New Church | ✅ live | | UDM Pro-stmary-history | UDM Pro | History Church | ← this change | | UNVR-oldhall | UNVR (Protect) | History Church | device only | | UCK G2 Plus-stmary lab | UCK G2 Plus | Lab | skipped | | UDM | UDM | (Aug 2025 backup) | skipped |
History Church LAN facts (from the UDM Pro-stmary-history overview):
10.10.10.0/24, gateway 10.10.10.1.10.10.100.0/24 (not monitored — guest traffic).98.172.31.24.mm2-2023) to 10.10.10.1 returns ~15–18ms consistently → on-prem routing is fine.
code lands. If the Azure S2S tunnel doesn't route 10.10.10.0/24, the History Church UniFi section will surface a connect ETIMEDOUT banner and that will be the signal to fix routing.
Naming: display name stays "History Church". "Old Hall" refers only to the UNVR device at that site (SSID is OldStMarysHallU, S2S tunnel name mentions OldHall).
Changes:
src/services/unifiStatusService.js — third controller stanza history-church reading UNIFI_HC_USER / UNIFI_HC_PASS, host defaults to 10.10.10.1.
config/config.json — History Church location now has `staticIp: "10.10.10.1" (so the location card can go Online when the UDM answers) and two devices: hc-udm-1 (monitored) and hc-unvr-1` (present but monitor toggle off; IP left blank for operator to fill in via Settings).
src/views/dashboard.ejs — env-prefix lookup extended to include history-church → HC so the missing-cred banner names the right vars.
src/views/settings.ejs — UniFi Client Discovery description now listsall three controllers and their env var pairs.
Problem: After shipping the "All UDM Clients" section, the dashboard showed a "History Church" section (which has no UniFi controller — it uses a static Cox IP) and silently hid the New Church section whenever env vars weren't picked up by the running process. Result: user couldn't tell if UNIFI_NC_* were missing, typo'd, or actually wrong credentials.
Fix:
unifiStatusService.js — added hasController(locationId) and isConfigured(locationId) helpers.
src/routes/dashboard.js + src/routes/api.js (/unifi/clients, /unifi/rescan) — only include locations with a controller.
src/views/dashboard.ejs — always render the section for controllers; if creds are missing, show an info banner naming the exact env var pair to set (e.g. UNIFI_NC_USER / UNIFI_NC_PASS) rather than hiding silently.
Problem: After the UDM Pro was wired up as the presence source of truth, the dashboard only showed devices explicitly listed in config/config.json. Every other client on the network (phones, chromecasts, unmonitored workstations, etc.) was invisible even though the controller sees them.
Fix: Expose the full UniFi client list per location on the dashboard as a collapsible "All UDM Clients" card, and add a Settings button to force an immediate rescan.
Files:
src/services/unifiStatusService.js — presence records now include an enriched clients[] array (hostname, IP, MAC, wired/wireless, VLAN, last-seen), sorted wired-first then by IP. New getClients(locationId).
src/routes/api.js — new GET /api/unifi/clients (all clients per location) and POST /api/unifi/rescan (force refresh + status recompute).
src/routes/dashboard.js — passes unifiClients to the view.src/views/dashboard.ejs — new collapsible "All UDM Clients" section perlocation with per-card Rescan button.
public/js/dashboard.js — refreshUnifiClients() polls /api/unifi/clients each auto-refresh tick; rescanUnifi() handles rescan-button clicks.
src/views/settings.ejs — global "Rescan All UniFi Clients Now" buttonwith per-location result summary.
How to use: Dashboard → each location card has a collapsed "All UDM Clients — <location>" section below the monitored-devices grid; click to expand. Rescan button on the section header or on the Settings page forces immediate refresh (bypasses the 60s polling interval).
Commit: 2f340f0
Problem: PC-L7480 (192.168.1.169) and other Windows workstations showed "Offline" at it.smosva.org even when they were on. The portal probes LAN devices via TCP socket connect only (Azure App Service blocks ICMP outbound), and Windows Firewall silently drops unsolicited SMB (445) / RDP (3389) packets. Both probes time out, dashboard reports Offline — misinformation.
Fix: Query the UDM Pro's /stat/sta client list and treat any device the controller sees as connected as Online, regardless of TCP probe result. Same fix automatically covers every LAN device — no per-device port tuning.
Files:
src/services/unifiClient.js (new) — UniFi OS + legacy controller login, fetchClients, fetchDevices.
src/services/unifiStatusService.js (new) — per-location session cache, refresh, isOnline({ip, mac}) lookup.
src/services/pingService.js — checkAll refreshes UniFi in parallel with TCP probes, attaches unifiAlive per device.
src/views/dashboard.ejs, public/js/dashboard.js — statusBadge treats unifiAlive as Online.
Enablement (operator, out of band): Requires env vars set in Azure App Service — UNIFI_PC_USER, UNIFI_PC_PASS for Parish Center UDM Pro (host defaults to 192.168.1.1), and optionally UNIFI_NC_USER / UNIFI_NC_PASS for the New Church CloudKey (192.168.5.15). Missing creds are a silent no-op — behavior is unchanged until enabled. The UDM local admin should be restricted to local access, role Site Admin – View Only (endpoint hit is read-only).
Status: Validated in production 2026-07-01. Parish Center UDM Pro credentials configured; PC-L7480 (192.168.1.169) confirmed flipped from false-Offline to Online at it.smosva.org. New Church controller credentials not yet configured — same fix will apply there once UNIFI_NC_USER / UNIFI_NC_PASS are set.
UniFi admin console note: On UDM Pro firmware ~10.4.x the admin management UI moved. It is not under UniFi OS "Identity" — it lives at https://192.168.1.1/network/default/admins/ (Network app → Settings → Admin Permissions → + Create New → Restrict to local access only). Save that URL for future admin changes.
Commit: 0396c07
Extended Settings page to add or remove device rows per location without editing config/config.json by hand. Populated New Church location with initial device inventory (USG Pro, USW Pro 48 PoE, CloudKey Gen2, six NanoHD APs).
Three commits landed together:
db6d7ed — Added iMac (192.168.1.216) and introduced per-device checkPorts so SSH (22) / VNC (5900) probes can target macOS devices.
f6a9cf2 — Added a "Monitor" toggle in Settings so only user-pickeddevices appear on the dashboard.
c77cc95 — Corrected device types and default probe ports based on MACvendor lookup during initial inventory.
Multi-commit debugging session while the site-to-site VPN was being validated:
394d91a — GET /api/debug/ping runs a raw ping from the container.af04225 — Use /bin/ping full path in that endpoint (PATH issues inApp Service).
8ba8667 — Endpoint now reports container network interface IPs and theoutbound source address used for the probe (routing diagnostics).
334b88e — GET /api/debug/probe?host=&port= for raw TCP probe results.ce29a3d — Switched from ICMP ping to TCP socket probe. ICMP is blocked outbound from Azure App Service; TCP probes work over the S2S VPN. Also set rejectUnauthorized: false for HTTP probes so self-signed certs on LAN devices don't fail the check. This is the change that made the UniFi fix above necessary — TCP probes fail on hardened Windows PCs.
ea81a1e — .gitignore for .claude/ session directory.Commit: 2008574
Added a Site-to-Site VPN card on the dashboard that reads the Azure Management API (Microsoft.Network/connections) for connection state and bytes-transferred counters. Uses AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET for the service principal.
Commit: 9614243
Added UDM Pro Max (192.168.1.1) and NVR (192.168.1.77) to Parish Center device inventory.
Fixing the GitHub Actions → Azure App Service pipeline:
bc1f29e — Removed slot-name (B1 tier doesn't support deployment slots).5f957a4 — Switched from publish profile to Azure CLI deploy for theLinux App Service.
fd43958 — Use azure/login@v1 with explicit credential params.Commit: af9378f
Node 20 / Express 4 / EJS scaffold. Three locations (St Mary Parish Center smosva.org, New Church smosva.com, History Church static-IP-via-Cox). No database — config in config/config.json, editable via /settings. Background ping loop (statusCache) polls at pingIntervalSeconds and serves cached results via /api/status for the auto-refreshing dashboard.