v2.3.0 Release Changes#1025
Merged
Merged
Conversation
The maps (DiscoveredClient via stat/sta) and Client Performance's online identify path only had v1 name/hostname, so name-less clients rendered as a raw MAC while Client Stats showed UniFi's friendly display_name (v2 active-clients). Plumb that display_name into both as the top of the fallback chain (DisplayName > Name > Hostname > MAC) via a dedicated per-connection labels-only cache (5 min TTL) that leaves the real-time active-clients call uncached.
… error (#1026) * Fix intermittent duplicate Wi-Fi Optimizer Health Issues entries GetSiteHealthScoreAsync assigned the fresh score to the shared _cachedHealthScore field and then mutated that field in place (MLO/6 GHz checks + EvaluateRules). With no synchronization, an initial load racing the Overview auto-refresh could interleave so both calls appended their findings onto the same score object, doubling every Health Issues entry (the '(2)' badges). Serialize refreshes with a semaphore and build the score in a local, publishing it to the field only once fully assembled. * Don't surface a disposed-client login as a console-connection error On reconnect (e.g. a restart while an agent-tunnel site's console is coming back up) the UniFiApiClient and its _authLock can be disposed while a LoginAsync is still in flight. The finally's _authLock.Release() then threw ObjectDisposedException, which bubbled up and showed as a 'Cannot access a disposed object' error in the Wi-Fi Optimizer console-connection warning box. Guard just that Release so a disposed-mid-login degrades to the normal transient retry instead of a scary error.
…he LAN Flow Map (#1027) * Absorb UDB device names and bridged-client throughput Two related gaps for UniFi Device Bridge (UDB) setups (e.g. a Protect camera wirelessly bridged onto the LAN): Names: UDB-bridged clients carry no user name/display_name, only an auto hostname, but expose a friendly name in unifi_device_info_from_ucore (e.g. '[Camera] Driveway'). Add that field to UniFiClientResponse and use it as a label fallback (display name > name > ucore name > hostname > MAC) on the 2D/3D map and Client Performance. Rates: a UDB uplinks over Wi-Fi with the bridged client on its single downlink port, whose own client counters UniFi reports as zero. Its real throughput is on the UDB's own port_table. Fold DeviceBridge into the mesh-AP backhaul synthesis (new contributor reading the bridge's downlink port, gated to DeviceBridge) so the UDB's uplink link shows the flow, and source the bridged client's leaf-link rate from that same aggregate (gated to bridge parents). Switches, their clients, and mesh APs are untouched. * Record UDB bridge rates to InfluxDB for historic map playback The live UDB rate fix routes through LanFabricAggregator + MonitoringLiveStats, which are in-memory only. Historic LAN-flow-map playback re-derives rates from the durable interface_counters measurement instead - which works for switches/ APs/mesh (their bytes are in an SNMP port or vwiresta series) but not for a UDB, whose throughput lives solely on its own port_table (a UniFi-API signal, never SNMP). So the UDB uplink and its bridged-client leaf were blank in playback. Persist each UDB's summed downlink port_table rate to interface_counters under a synthetic 'bridge-downlink' series (BridgeInterfaceRecorder), from the shared recording path so both directly-monitored and agent-relayed sites record it. Teach the historic MeshBackhaul and WiredClient resolvers to read that series (the latter via the existing BridgeParentMac marker), matching the live directions. No new measurement; switches/APs/mesh playback is unchanged. * Use a generic example device name in doc comments * Keep UDB device badge symmetric in historic playback The historic node-badge fabric-sum treated the UDB's synthetic bridge-downlink series as switch fabric, summing its single directional flow into ingress/egress (low in, high out) - so a bridge looked asymmetric during playback while live showed it symmetric. Exclude the bridge-downlink series from the fabric-sum so the UDB falls back to adjacent-link summing, matching the live badge. Real switches never carry that series, so they're unaffected.
… transit jitter, off-path hop guidance (#1028) * Upstream discovery: always attach the Level 3 transit witness when near-transit Previously the anycast witness (4.2.2.2) was injected only as a fallback, when no real AS3356 router cleared the reachability gate. When real Level 3 hops respond, they'd suppress it - so a site with pingable Level 3 hops never got the stable anycast anchor, and ISP Health's Lumen transit signal drifted POP-to-POP across discovery runs. Drop that suppression: whenever a curated transit ASN is genuinely near-transit (and its witness itself clears the gate), attach the witness alongside any real routers. Real hops are kept, never replaced - the anycast endpoint is usually a more distant POP than the closest real hop. Its samples fold into the ASN's transit tier in ISP Health automatically. Same-address dedup on write means a hand-added witness at the same address is respected, not duplicated. Generalizes to every entry in the witness table. * Upstream discovery: traceroute the transit witness so it carries ancestry The witness was injected as a ping-only target, so it landed in UpstreamDiscoveries with empty AncestorHopIps and could never satisfy FarClusterRoutesThroughNear - meaning ISP Health could never use the witness's clean end-to-end jitter to absolve a jittery near hop it routes through, even though the whole absolution mechanism already exists for traced destinations. Trace the witness at injection and append the result to the discovery traces. PersistHopOrderAsync then records the real transit hops preceding it as its ancestry, unchanged. The proof is honest: ancestry comes from the anycast endpoint's actual path, so an absolve only fires when that path genuinely traverses the monitored hop; a different POP yields no overlap and no absolve. No schema change (AncestorHopIps already exists); scorer-side absolution and the ancestor gate are already covered by IspHealthScorerTests. * ISP Health: destinations absolve transit jitter + involvement-weighted Transit Health Arm A: transit ASNs are now absolved by any monitored destination (or another transit ASN) proven to route through them, routes-through gated on stored ancestry - the same clean-end-to-end upper-bound logic already used for ISP hops. Folded with the existing within-ASN far-cluster absolution; strict, never on faith. Arm 4: Transit Health is now a weighted average - each transit ASN weighted by its internet-host involvement (share of monitored destinations proven to route through it): floor 0.25 + 0.75 * reach/maxReach, so a graded ASN never zeroes but a side-path transit counts as little as a quarter of the main-path one. When no host is attributable (transit invisible on destination traces), all weights collapse to equal - identical to the prior plain average. A fraction-icon tooltip surfaces the share/weight, shown only when involvement differentiates. No schema change - both compute from ancestry already loaded in the scorer. Tests cover destination->transit absolution and involvement weighting. * ISP Health: floor transit involvement at 25% on peered sites (true zero) The involvement weighting conflated two different zeros: "can't attribute" (no hop order / no destinations) and "attributed, and the answer is zero" (a peered site whose destinations cross no transit). The first should fall back to equal weight; the second is a true-zero involvement and should sit at the 25% floor. Gate the involvement weighting on attribution being KNOWN (hop order + at least one destination) rather than on maxReach > 0. When known but no transit carries a host, every transit ASN floors at 25% and shows the fraction-icon tooltip ("0 of N hosts") instead of silently reverting to equal weight with no icon. The dimension score is unchanged where the flooring is uniform; the point is correct, visible attribution. * ISP Health: blend uninvolved transit toward neutral, pie-slice involvement icon Scoring: a transit ASN now contributes weight*own + (1-weight)*100 to Transit Health, involvement-weighted - so a transit carrying none of your hosts can't drag the number (its uninvolved share reads neutral), while the 25% floor keeps a graded ASN from vanishing. Fixes the case where a peered site's unused transit (e.g. Cogent at 74) pulled Transit Health down via the plain average, even though it carries zero monitored hosts. Involvement now lives on IspAsnHealth as the single source for both the dimension and the card. UI: replaced the wrapping text glyph with a pie-slice SVG that fills to the involvement weight, kept on the same line as the ASN name (the factor label is a flex column, so name+icon are now grouped in a row), and added it to the Networks on Your Path cards too. data-tooltip carries the host fraction and weight. * ISP Health: transit involvement tooltip explains the 25% floor (asymmetric routing) The floored-transit tooltip now makes clear the ASN is still scored, not dropped: our involvement is measured on the forward path (traceroute), but a transit we never cross outbound often carries the return path from popular services (hot-potato / asymmetric routing) - so it's held at the 25% minimum rather than zeroed. The carrying-hosts tooltip is clarified to "on the forward path". * ISP Health: trim transit involvement tooltip to one line * ISP Health: transit tooltip says 'internet targets', not 'hosts' * ISP Health: flag off-path jittery ISP hops with a disable hint linking to Latency targets An ISP-network hop that answers pings but appears on no discovery trace (HopNumber 0 - e.g. an OLT/CMTS that ICMP-deprioritizes traceroute) isn't a forward-path jitter signal, yet its jitter still drags ISP Network. When such a hop is off the traced path AND its jitter score is a real deficit (<=70), and it isn't the graded nearest hop, surface a "Not on path" hint on its ISP Health row with a tooltip; clicking it switches to the Setup tab and highlights the target in Latency targets to pause it. Detection reuses the existing per-target hop distance (HopNumber 0 = never traced); no new discovery data. IspHealthPanel raises OnReviewTarget; Monitoring maps the TargetId to the row and jumps once the card mounts. * ISP Health: disable hint jumps to the right tab; commonize + align the hop badges - Fix: the "Not on path" hint switched to the Setup tab, but Latency targets lives on the Network Performance tab (same as JumpToLatencyTargetAsync). Jump there so the target actually highlights. - CSS: extract a shared .isp-target-badge base for the "Lowest RTT" and "Not on path" badges (margin, padding, radius, font, uppercase, spacing, vertical-align); only color/cursor/hover stay per-badge. Bumps margin to 0.5rem and vertically centers them against the hop name. * TODO: AWS S3/DynamoDB endpoints for transit trace exposure (Monitoring) * TODO: DynamoDB regional endpoints (ICMP-pingable) for transit trace exposure
…with SFP module attachment (#1030) * ONT provider: Network Optimizer Custom (HTTP JSON) with SFP module attachment - New netopt-custom ONT provider polling a vendor-neutral PON stats JSON endpoint (contract doc in docs/features/netopt-custom-pon-contract.md) - Attach-to-SFP-module mode: polled on the gateway SFP collection cycle and merged onto that module's sfp measurement; pon_link_status / fec_errors / bip_errors reuse the ont measurement's field names and encodings - Standalone mode feeds the ont measurement and ONT alerts like any provider - Settings - ONT Device Monitoring: provider entry + Attach to SFP Module selector (OntConfigurations.AttachedSfpId migration) - SFP Stats: conditional PON errors / GEM frames / host link delta charts and PLOAM details table for supplemented modules - ONT Stats: FEC / BIP per-interval delta chart (all providers reporting the counters) * ISP Health: uniform hop badge height and centering * Attached ONT configs: exclude from standalone ONT UI (Dashboard, ONT Stats, ISP Health) An ONT config attached to an SFP module surfaces as PON data on that module (SFP Stats), so it must not also appear as its own ONT device. Add GetStandaloneConfigsAsync (filters AttachedSfpId) and use it for the ONT Stats tab, Dashboard ONT card, and ONT chart endpoint; exclude attached configs from ISP Health PON candidates too. * SFP ONT card: show PON status and absolute BIP/FEC/drops when available Cache the latest PON supplement (link status + cumulative BIP/FEC/GEM-dropped) in SfpLiveStats during the SFP poll, and surface it on the SFP ONT Connection card: PON Status in the Connection box (same format as the external ONT card) and absolute BIP/FEC/Drops in the Stats box, each shown only when present. * SFP ONT card: combine BIP/FEC/Drops into one row to save space * SFP ONT card: reorder PON stats, adaptive FEC/HEC, even columns - PON Status moves directly below Link Type in the Connection box - BIP/FEC/Drops error counts move into the Connection box - Middle error slot is adaptive: shows FEC when payload FEC is enabled, HEC (the always-on header-error signal) when FEC is disabled, label flips to match - SFP Voltage moves to the Stats column only when a PON supplement is present, keeping the two columns even * SFP/ONT charts: lazy-mount PON + FEC/BIP charts, filter-aware PON section - Don't create the PON charts (SFP Stats) or the FEC/BIP errors chart (ONT Stats) until a module actually reports supplemental data. Setups without supplemental polling - the large majority - never mount instances they'd never see. - PON section and its detail table now follow the filter: shown only while a PON-capable module is selected, table lists just the selected modules. * SFP Stats: fix filter-chip breakage and PON chart sizing; drop TX idle - PON section refresh is now fire-and-forget off updateVisibility, so an ApexCharts error can never abort the synchronous chip re-render (was leaving chip highlight desynced and blocking re-selection) - Show the PON section before mounting/updating its charts so ApexCharts sizes them correctly instead of loading blank until the next tick - Rebuild PON series from only the visible modules each refresh (no per-series show/hide bookkeeping) - Drop TX idle from the GEM chart - it's an upstream-utilization indicator, not a health signal, and dominated the scale * Review fixes: per-endpoint poll gate, awaited PON alert eval, resilient chart refresh - NetOptCustomPonOntProvider: key the request gate per (host, port) instead of one global semaphore, so distinct endpoints/sites poll in parallel - CollectPonSupplementalAsync: await the PON alert evaluation inside its own try/catch (was fire-and-forget; unobserved exceptions escaped the poll catch) - sfp/ont charts: guard the PON/errors chart refresh in loadAndUpdate so a chart failure can't abort badge/stats-table rendering * ONT alerts: add BIP and adaptive HEC spike alerting - OntAlertEvaluator now evaluates BIP (always-on) and, when payload FEC is disabled, HEC in place of FEC - mirroring the SFP ONT card's adaptive display. Shared per-poll spike helper, reset-guarded like the existing FEC check. - Seed ONT: BIP Error Spike and ONT: HEC Error Spike rules (disabled by default). - Attaching augmented PON polling to an SFP ONT enables the BIP/HEC rules immediately (AlertRuleAutoEnable.EnablePatternsAsync); existing ONT users also get them via the standard freshly-seeded startup pass. Closes the gap where a FEC-disabled attached ONT had no error-counter alerting. - Feed BIP from the standalone ONT path and BIP/HEC/FEC-enable from the attached path. * ONT alerts: tune BIP/HEC thresholds, make BIP FEC-state-aware - HEC spike threshold 100 -> 10 (uncorrectable header errors; healthy reads ~0) - BIP spike threshold is now FEC-state-aware: strict 25/poll when payload FEC is disabled (every BIP is uncorrected data loss), relaxed 1000/poll when FEC is on or unknown (BIP counts pre-FEC line errors FEC corrects, so a healthy link at the normal operating point reads hundreds/poll) * ONT alerts: deep-link to the triggering ONT Alerts now carry a per-ONT SourceUrl instead of a fixed /monitoring?tab=ont: standalone ONTs link to the ONT tab focused on that device (?ont=id), attached SFP ONTs link to SFP Stats focused on that module (?sfp=mac:port), so clicking an alert lands on the tab that actually shows it. * TODO: note wrap-aware PON counter deltas as a low-priority follow-up * SFP Stats: group PON error charts, GEM frames last Reorder the PON section to PON Errors -> Host Link Errors -> GEM Frames so the two error charts sit adjacent (faster upstream-vs-host fault localization, consistent units) with throughput last.
* ISP Health: grade direct-peered internet destinations as an IX Peering entry in Transit Health When internet targets are reached directly over the access ISP's peering/IX (no transit ASN on the forward path, and a small best-case delta beyond the first clean ISP hop), grade their measured end-to-end quality as a synthetic "IX Peering" transit entry instead of leaving the Transit dimension averaging against the neutral-100 fill. Selection requires BOTH arms: no transit on the path (AS-path, reusing the routes-through ancestry) AND best-case delta under 10 ms. Far-but-untransited peers and low-latency-but-transited destinations are both excluded. No entry appears where every destination crosses a transit provider. Adds two scorer tests. * ISP Health: grade IX Peering per-destination and average, not pooled Pooling all peered destinations' samples into one series manufactured cross-target variance (targets at different RTT baselines look like one swinging series), craters the stability sub-score, and let one target's jitter tail dominate the pooled P95. Grade each peered destination on its own series and average the grades, so one flapping peer counts 1/N. Log each member's grade at Debug for diagnosis. * ISP Health: IX Peering proximity jitter absolution + weight-desc network ordering Proximity absolution: when peered destinations reach the internet at a fraction of the median transit ASN's RTT, buy back part of the IX Peering jitter penalty, scaled by the RTT advantage and bounded to jitter's own weighted share of the grade (never masks loss, stability, or a real congestion penalty). Where transit is as close as peering, advantage < 1 and no bonus applies. Ordering: sort the transit/IX entries by involvement weight descending (nearer network breaks ties) for both the Transit Health factor list and Networks on Your Path; Access renders first as before. * ISP Health: Transit Health = plain involvement-weighted average (drop neutral-100 blend) The dimension blended each transit ASN's uninvolved fraction toward a neutral 100 (effective = w*own + (1-w)*100), which inflated Transit Health above every real entry (e.g. entries 88/73/89 -> 90) and re-introduced the phantom-100 the IX Peering entry was meant to eliminate - masking a real congested-but-off-path transit up toward 100. Replace with a plain involvement-weighted average of the entries' own scores: the dimension now always lands within its entries' range, and a congested off-path transit dings it lightly (0.25 floor) rather than hiding at 100. Floor unchanged (return-path/failover accountability).
Add a start line and, on the no-match path, log the actual CNAME/A records returned so logs show what the resolver gave back for verify.controld.com (it only carries the ControlD record when the query traverses ControlD). Makes on-gateway ctrld detection diagnosable.
…lay fallback (#1029) * Nokia ONT: keep-alive connection fallback for firmware that 401s getUpdateinfo (#929) Some XS-010X-Q firmware binds the auth session to the login TCP connection, so presenting the session cookie on the separate getUpdateinfo socket (forced by the per-request ConnectionClose) is rejected 401 -> /login.html despite a successful login. Add a fallback that retries login + getUpdateinfo over a single shared keep-alive connection (MaxConnectionsPerServer=1) when the fresh-connection path yields no RX reading. Also log the LoginForm Set-Cookie header to reveal whether a non-working unit uses a header cookie with a different name. * Nokia ONT: send browser Referer/Origin/X-Requested-With on GponForm calls (#929) HARs from two testers show the successful getUpdateinfo carries Referer: /moreinfo.html, Origin, and X-Requested-With: XMLHttpRequest, and no cookie at all. A T-Fiber/Metronet CLEI variant gates the call on that Referer and 401s to /login.html without it, while the firmware this was first built from accepted the sessionid cookie. Send both the cookie and the browser headers so a single request shape works for both, no per-firmware branching. Supersedes the keep-alive connection fallback (the connection-binding theory the HARs disproved). Keeps the Set-Cookie diagnostic log. * Nokia ONT: page-walk auth flow for CLEI firmware, cached per config (#929) Confirmed by curl on a T-Fiber/Metronet CLEI XS-010X-Q: getUpdateinfo is gated behind the forced PON-password walk (GET /ponpasswd.html, POST ponpasswd_GetConfig, GET /moreinfo.html) with the session cookie on every step. Add that as a PageWalk flow, tried after the Direct flow and cached per config (ConcurrentDictionary keyed by OntConfiguration.Id, re-detected after restart), same idiom as NetgearCmProvider. Direct stays the bare cookie-only getUpdateinfo that is the only flow confirmed working on real hardware (Liosnel's), tried first so it's never regressed; only a box that fails Direct pays for the walk probe, once, until cached. Browser headers (Referer/Origin/X-Requested-With) and User-Agent apply to login and the PageWalk; Direct's data call is left untouched. * Nokia ONT: gate User-Agent to browser flows, fresh login per flow (#929) Ground-truth check of v2.1.1/v2.2.0: both ship a cookie-only getUpdateinfo (no browser headers), v2.1.1 on a plain client, v2.2.0 adding ConnectionClose. So the Direct data call must carry no User-Agent either - move the UA off the shared client and behind the browserHeaders gate, so it rides login and the PageWalk but not the Direct getUpdateinfo, which now matches the shipped request exactly. Also give each flow its own fresh login: a Direct probe that 401s on the CLEI variant leaves a forced-PON-password session state that could poison a following PageWalk if they shared one login. Matches how the confirmed curl ran (one login, then walk). * Nokia ONT: raw-socket curl replay for firmware that rejects HttpClient framing (#929) The picky CLEI firmware accepts the login+getUpdateinfo sequence from curl and browsers but 401s the logically identical HttpClient requests. Byte captures show the framing delta: HttpClient omits User-Agent and Accept, appends ;charset=utf-8 to Content-Type, orders content headers last, and adds Connection: close - none fully controllable through HttpClient. Replace the PageWalk flow with CurlReplay: a raw TCP replay of the tester-proven curl script, byte-for-byte (headers, order, casing), fresh connection per request. Direct stays first for uncached configs and is reverted to the exact shipped v2.1.1/v2.2.0 bytes (the one flow confirmed working on real hardware). * Nokia ONT: disable handler cookies so the sessionid header survives (#929, thanks @jakerobb) The picky firmware answers login calls with a malformed 'Set-Cookie: Path=/; HttpOnly' header. A default HttpClientHandler's CookieContainer parses 'Path=/' as a literal cookie and then silently substitutes it for the hand-set 'Cookie: sessionid=...' header, so the device never saw the session id - the actual root cause jakerobb isolated and verified on his unit. UseCookies=false makes the Direct flow work on that firmware; the CurlReplay raw-socket fallback stays as defense in depth (immune to handler rewriting by construction).
…and report agent(s) offline when it's down (#1032) * Agent status: don't report online off REST heartbeats when the tunnel is down When the server offers an agent tunnel, an agent that only ever REST-heartbeats has a dead or unpublished tunnel (e.g. the gRPC tunnel path isn't reverse-proxied) with every tunnel-dependent feature down. Heartbeat freshness alone no longer counts as online in that case - only an open tunnel, or a short reconnect grace after a previously-connected one drops. When the server offers no tunnel at all, REST heartbeat freshness still means online (single-box/legacy deployments). LAN speed-test target resolution keeps the looser reachability check (IsReachableForLanTest): those tests hit the agent's nginx directly, not the tunnel, so a heartbeat-only agent is still a valid target. * Review: keep the client speed-test placeholder on the LAN reachability check The Client Speed Test target placeholder is documented to mirror SiteSpeedTestTargetResolver's pick, which resolves via GetOnlineAgentLanIpAsync (IsReachableForLanTest). Splitting IsAgentLive left the placeholder on the stricter status check, so a heartbeat-only agent would show the generic example instead of the real auto-resolved LAN target. Point it at the same reachability predicate and refresh a now-stale comment. * Rename IsReachableForLanTest -> IsReachable The predicate only checks reachability (open tunnel or fresh heartbeat); it does not verify the agent actually hosts a LAN speed test (that's an opt-in agent flag the server has no signal for yet - separate TODO). Name it for what it checks and document the limitation, so it no longer implies a capability guarantee it can't make. Pure rename, no behavior change. * Never redirect gRPC from the canonical-host middleware The canonical-host redirect (REVERSE_PROXIED_HOST_NAME) runs on the shared pipeline, so it also sees the agent tunnel's gRPC requests on the HTTP/2 listener. A gRPC stream cannot follow a 302, so when a reverse proxy forwards the tunnel path without presenting the canonical Host (e.g. Caddy proxying to the https://localhost tunnel upstream sends Host: localhost), the Connect stream was redirected to death - while REST heartbeats still landed and kept the agent showing online. Skip the redirect for application/grpc requests: gRPC is machine-to-machine and has no canonical-host concern.
Read-only telemetry provider for the Zyxel PMG3000 GPON-SFP stick (a MaxLinear/T&W-based unit) over its HTTP CGI API. Basic auth (default admin/1234), GET /cgi/get_gpon_info for optics/link and GET /cgi/get_sn for serial identity. Bodies are JavaScript object literals, not strict JSON, so a small provider-local lenient tokenizer decodes them; no new dependency. Maps line_status via the shared O1-O7 parser plus Rx/Tx power, temperature, voltage and bias current onto OntStats. get_gpon_info is the required endpoint; get_sn is best-effort and never logged (it carries cur_pass). Fixture-driven unit tests against a scrubbed live capture. The existing Realtek provider was ruled out first (endpoints 404, form login vs this device's Basic auth); see the design spec for the evidence.
- get_sn (best-effort serial) timeout no longer discards optical telemetry: an HttpClient.Timeout surfaces as OperationCanceledException with the caller token not cancelled; rethrow only on genuine caller cancellation, otherwise swallow (inner) or return null (outer) per the IOntProvider contract. - line_status is matched as an exact single ordinal 1-7 (new MapLineStatus) instead of the shared substring parser, so a malformed value like "51" no longer substring-matches O5 and gets misreported as a healthy Up link. - Regression tests for both.
The Add/Edit ONT form showed the same generic 192.168.1.254 host placeholder for every provider. Make it provider-aware, mirroring the existing port default: selecting the Zyxel GPON-SFP provider now placeholders and prefills (when the host field is empty) the stick's fixed management IP 10.10.1.1, so the correct value is suggested instead of a meaningless generic one. Other providers keep the generic placeholder and are never prefilled.
…-ont # Conflicts: # src/NetworkOptimizer.Web/Components/Pages/Settings.razor # src/NetworkOptimizer.Web/Program.cs
Add Zyxel GPON-SFP (PMG3000) ONT provider thanks @Optic00
Suggest a known default Host/IP when selecting an ONT provider whose device ships on a fixed management address (AT&T 192.168.1.254, 8311 192.168.11.1, Quantum 192.168.0.1, Telekom/Nokia 192.168.100.1), matching the existing port-default behavior. Realtek, netopt-custom and generic stay blank (no single sensible default). Rename the Zyxel option/display name to "Zyxel GPON-SFP PMG3000 (HTTP)" so only the transport sits in the trailing parenthetical.
- Prefill the ONT Host for the default provider. att-gateway is selected by default and never fires OnOntProviderChanged, so its known host was only shown as a placeholder, not filled. Centralize the fresh-form config in NewDefaultOnt() and seed Host/Port from the provider defaults. - Generic host placeholder now 192.168.100.1 (the most common ONT/modem management address) instead of 192.168.1.254. - Add-a-Site wizard: reset the completed "... is ready" screen back to a fresh form when a site's Configuration is opened from the Sites table, so the stale success state does not linger. No-op mid-setup, so input is never discarded.
Add a close button to the upper-right of the site config panel (Settings - Multi-Site, the row that opens from Configuration) so it can be dismissed without hunting back for the Configuration toggle. Collapses via the existing ToggleAgents handler.
…es (#1034) * ISP Health: score jitter at P90 instead of P95 Make the jitter scoring percentile a config option (AsnJitterScoringPercentile, default 0.90) instead of a hardcoded 0.95 in ScoringJitterOf. Drives per-ASN/hop jitter scoring, the path jitter floor, absolve/witness comparisons, the ISP/transit cap, and the cards - one lever. IX Peering members are graded through the same GradeAsn path, so they move with it. P95 double-counted the jitter tail with the separate congestion detector (which already catches intermittent bursts off median jitter + p90 RTT); P90 reads the typical tail and relieves the harshness without going blind like P50. Scoring is floor-relative, so numerator and floor track the percentile together. Rename P95JitterMs -> ScoredJitterMs (percentile-neutral) and the "P95 Jitter" UI label -> "P90 Jitter". Add a CI-safe replay harness (JitterPercentileReplayTests, no-ops without ISP_HEALTH_REPLAY_DIR) that isolates the jitter arm at P50/P90/P95. * ISP Health: caveat tooltip on reach-0 transit scores below 80 A transit ASN that no monitored internet target routes through on the forward path is held at the 0.25 involvement floor. When such a network scores below 80, the score text (on both Transit Health and the Networks on Your Path card) now carries a tooltip noting the low score may just be its routers deprioritizing ICMP rather than real path trouble - it's kept in view because it may carry the return path or a failover route. Score values also get a default cursor instead of the text I-beam. * ISP Health: tighten reach-0 transit caveat tooltip copy * ISP Health: fix caveat tooltip clause join * UniFi Console Connection: gate awaiting-agent state on a configured console
…et wizard on site removal - The default-site "New Agent Token" flow hardcoded LanSpeedTest=false, so its generated install command could never include --lan-speed-test. Add a LAN speed test checkbox (mirroring the wizard) that live-updates the command; non-default sites already prompt via the wizard. - After removing a site, reset the Add-a-Site wizard's completed screen (same ResetIfComplete used when opening a site's Configuration) so it doesn't linger.
Removal used ResetIfComplete (step-4 only), but you can delete a site while the wizard is on the agent step (step 3) for that very site. Add a public Reset() that clears the form from any step and use it on removal; opening a site's Configuration keeps the conditional reset so mid-add input isn't discarded.
…ll, tidier output (#1035) * agent installer: auto-remediate AppArmor-confined nginx for LAN speed test Some distros ship an enforcing AppArmor profile on the nginx binary that only permits nginx's stock paths, so our dedicated speed test master - which keeps its pid, logs, and webroot under the install dir - is denied even as root (a MAC denial, not file perms), and the LAN speed test never serves. The installer now, ONLY after `nginx -t` has already failed AND the kernel log shows AppArmor denying our install dir, adds a local, additive override under /etc/apparmor.d/local granting the confined nginx access to the install dir, reloads just that one profile, and re-tests. It never edits the vendor profile; a parse error makes apparmor_parser abort and keep the running profile, so a bad override can't unconfine the host's own nginx. If the profile can't be found or doesn't consume the local include, the override is inert and we fall back to a precise manual hint (local override, or `aa-complain`). Reactive by design: on any host where `nginx -t` already passes, none of this code is reached, so currently-working installs are byte-for-byte unchanged. The speed test failure remains non-fatal - the agent itself is unaffected. * agent installer: correct AppArmor fix hint for non-standard nginx profiles The fallback hint hardcoded the profile filename (usr.sbin.nginx) and pointed aa-complain at the resolved binary, both wrong when the confining profile is named differently (e.g. /usr/bin/nginx) or isn't file-backed under /etc/apparmor.d (snap/custom profiles - Mother's case). Now derive the real profile name from the denial record, search the snap profile dir as well as /etc/apparmor.d, and only attempt the local-include override when the profile is actually a file under /etc/apparmor.d (the only place local/ is consumed). When it isn't, the hint leads with `aa-complain '<real-profile>'` instead of a bogus local-override path. * agent installer: --uninstall, root-worker for non-traversable dirs, safe override - Add `--uninstall` to the bare-metal installer (parity with the gateway one, and the README already implied it existed). Stops both services BEFORE deleting their unit files - so it never leaves a running "ghost" unit like a manual rm does - then removes the units, enable symlink, our AppArmor override, and the install dir. Leaves the host's own nginx and AppArmor profile untouched. Documented in the agent README. - Fix LAN speed test 403 when the install dir isn't world-traversable (e.g. under /root, mode 700): unprivileged nginx workers can't reach the webroot. Detect a non-traversable ancestor chain (handling the sticky bit, so /tmp is fine) and run the workers as root only then; the /opt default keeps the safer unprivileged user. - Make the AppArmor local override additive and marker-bracketed instead of clobbering the whole /etc/apparmor.d/local file, so a user's own rules there survive and --uninstall can remove exactly our block. * agent installer: find nginx AppArmor profiles under /usr/share/apparmor.d too Some distros ship the nginx AppArmor profile under /usr/share/apparmor.d (with /etc/apparmor.d reserved for local overrides) rather than in /etc/apparmor.d, so the auto-fix couldn't find it and fell back to the hint. Search that dir as well, and attempt the local override for a profile found there (the override still lives under /etc/apparmor.d/local, the standard include base; we reload the profile source wherever it ships). The nginx -t re-test still gates success, so if the profile has no local/ hook we fall back to the (now simpler) complain-mode hint. * agent installer: don't match/reload AppArmor cache files; skip cache on reload (-rT) Mother's nginx profile ships no findable source file - only a compiled cache at /usr/share/apparmor.d/cache/usr.bin.nginx. The broadened search matched that cache and apparmor_parser -r on a binary cache produced the confusing 'reload failed'. Exclude cache/ (and local/) from the profile search so an unfindable source now falls straight to the clear aa-complain hint, and reload with -rT so a read-only cache dir can't fail the reload on appliance distros. * agent installer: detect aa-complain in the hint; scope uninstall's summary - AppArmor hint now detects aa-complain: shows it when apparmor-utils is present, otherwise the tool-free, source-free securityfs unload (echo profile > .remove) - which works on appliance distros that ship neither aa-complain nor a profile source file (the Mother case). - --uninstall summary only mentions nginx/AppArmor when they were actually involved: a monitoring-only install (no --lan-speed-test) never touches nginx, so it just says "agent removed"; a speed-test install says nginx is untouched; and it notes the AppArmor override only when one was actually removed. * agent installer: cleaner, structured output Silence the curl progress tables (downloads are fast; -fsSL), and add colorized, sectioned output - a bold title, per-section dividers + "==>" headers, "✓" for successes, "⚠" for warnings, dim detail lines. Colors/markers collapse to plain text when stdout isn't a terminal, so piped/logged output stays clean. Suppress the systemd "Created symlink" noise with `systemctl enable --quiet`. Same for the --uninstall teardown output. * agent installer: AppArmor hint gives persistent guidance only Drop the temporary securityfs unload (echo profile > .remove) from the fallback hint - it evaporates on reboot, which is useless in a production deployment. The hint now frames the speed test as optional (agent + monitoring unaffected) and points to persistent remediation only: aa-complain (edits+reloads the profile, so it survives reboot) when apparmor-utils is present, otherwise a persistent policy exception added by the host admin. * agent installer: AppArmor changes are opt-in (--configure-apparmor) The installer no longer modifies the host's AppArmor policy by default - an app installer silently editing a security team's policy is a non-starter for enterprise deployments. The scoped local override now only runs under the new --configure-apparmor flag. Default path: detect, stay non-fatal (agent + monitoring unaffected), and the guidance tells the operator to re-run the install command with --configure-apparmor to add a persistent, scoped exception. When the flag IS given but the host ships a source-less / hook-less profile (no scoped exception is possible), the guidance says so plainly and defers to the admin rather than pretending or hacking. Documented in the agent README. * agent installer: make it obvious nginx is only for the LAN speed test Clarify in --lan-speed-test / --configure-apparmor help and the README that nginx is used solely to serve the speed test - core monitoring never touches it - so a monitoring-only user doesn't read the AppArmor/nginx handling as something central. * agent installer: trim the nginx/apparmor help + docs wording * agent installer: sudo in the nginx diagnose hint (runs as non-root later) * agent installer: spruce gateway + docker installer output too Same structured, colorized output as install-native (title, section dividers + "==>" headers, "✓"/"⚠", dim detail; collapses to plain text when not a TTY). Output-only: no logic, conditions, curl targets, compose, or systemctl behavior changed. Gateway also silences the binary download meter (-fsSL) and the systemd "Created symlink" line (enable --quiet; verified accepted on systemd 247). * agent README: fix flag scoping + note AppArmor on the speed test - --uninstall and --configure-apparmor are bare-metal-only; they were listed under "Both scripts accept" but the Docker installer has neither. Split them out. - Add a short note in "LAN speed test serving" on AppArmor-confined nginx: the page may be blocked (monitoring unaffected), re-run with --configure-apparmor for a scoped exception, else an admin grants it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rolls up
devintomainfor the v2.3.0 minor release, headlined by ONT Device Monitoring.Monitoring
Live View
Bridged device names on the LAN Flow Map - A device bridged onto the LAN through a UniFi Device Bridge (e.g. a UniFi Protect camera) carries only an auto-generated hostname, so it rendered as that raw hostname on the 2D/3D LAN Flow Map. It now resolves the friendly name the console keeps for it, reading as e.g. "[Camera] Front Door", with the bridge itself shown as "[Bridge] ...". The same console-name fallback applies to Client Performance and its device picker, so a bridged camera reads as its friendly name there too rather than a MAC.
UniFi Device Bridge throughput on the LAN Flow Map - A UDB uplinks over Wi-Fi with its bridged client on a single downlink port, and UniFi reports that client's own wired counters as zero, so both the bridge's uplink link and the bridged-client leaf showed no throughput. Both now show the real rate, read from the UDB's own port table and folded into the existing mesh-AP backhaul synthesis (gated to bridges, so switches, their clients, and mesh APs are untouched). It works in live view and in historic timeline playback (the bridge rate is persisted so the scrubber can replay it), for both directly-monitored and agent-relayed sites.
ISP Health
IX Peering in Transit Health - When your monitored internet targets are reached directly over your ISP's own peering/IX - no transit provider on the forward path, and a low best-case latency beyond the first hop - they're graded end-to-end and surfaced as a single IX Peering entry (shown as Direct peering on the Networks on Your Path card). It reflects the real measured jitter, loss, and stability of those peered paths, so a mostly-peered connection gets a Transit Health backed by actual measurement rather than an assumed value. Each peered target is graded on its own and averaged, so one flapping anycast doesn't drag the whole entry; and where your peering reaches the internet at a fraction of the transit RTT, the entry earns back part of its jitter penalty for that proximity. A connection where every target crosses a transit provider (a rural backhaul) shows no IX Peering entry.
Destination-absolved transit jitter - A transit ASN's jitter is now discounted by any monitored internet target (or a deeper transit ASN) proven to route through it, the same clean end-to-end upper bound the ISP tier already used. An ICMP-deprioritized transit router no longer gets penalized for control-plane noise its forwarded traffic never sees. Strict: only where stored ancestry proves the path.
Involvement-weighted Transit Health - Each transit network counts toward Transit Health in proportion to how much of your internet actually rides it (the share of monitored internet targets whose forward path crosses it), and IX Peering and the transit networks are ordered by that weight (Networks on Your Path leads with your Access networks). The transit you use dominates; a side-path or backup transit is de-weighted to a 25% floor so its problems ding the number only lightly - but its real grade always counts, since it may be on your return path or a failover route a forward traceroute can't see. Transit Health is a straight involvement-weighted average of those real scores, so it always reads within the range of the networks listed under it.
Involvement pie icon - Transit factors and the Networks on Your Path cards show a small pie filled to the ASN's involvement weight, with a tooltip stating how many internet targets ride it on the forward path and its resulting weight (and, when none do, that it's held at the floor because it's likely the return path from popular services, asymmetric routing a forward traceroute can't see).
Off-path hop disable hint (ISP Network) - An ISP hop that answers pings but appears on no discovery trace (e.g. an OLT/CMTS that deprioritizes traceroute) and whose jitter is degrading its score now shows a "Not on path" hint. It links straight to Latency targets on Network Performance and highlights the hop so you can pause it instead of letting phantom jitter skew ISP Network.
Upstream path discovery
ONT Device Monitoring
Deep PON-layer visibility for a fiber ONT, folded into the SFP module you already monitor.
Network Optimizer Custom (HTTP JSON) provider - a new ONT provider that pulls PON-layer stats from an ONT over a plain HTTP JSON endpoint: PLOAM activation state, GTC status and counters, GEM frame and allocation counters, bridge-port discards, and host-link counters. The JSON contract is vendor-neutral and documented, so anyone can implement it for their own ONT hardware.
Attach an ONT to a monitored SFP module - a Network Optimizer Custom config can attach to a monitored SFP module (Settings - ONT Device Monitoring). Attached ONTs are polled on the gateway's SFP cycle and their PON metrics fold into that module's existing SFP series, so the module's optics and its PON internals form one picture instead of appearing as a separate ONT device on the Dashboard, ONT Stats, and ISP Health.
SFP Stats: PON charts - for a supplemented module, new per-interval charts for PON errors, host-link errors, and GEM frames, plus a PLOAM detail table (state, ONU ID, FEC DS/US, response time, ONT uptime). The whole section only appears for a module that has the supplemental data and is selected in the filter, so it stays out of the way for the majority who won't use it.
SFP ONT card (Dashboard and ONT Stats) - now shows PON Status and a combined BIP/FEC/Drops counter when available. The middle counter shows HEC (the always-on header-error signal) instead of FEC on a link running with payload FEC disabled, where the FEC counters are meaningless.
ONT Stats: FEC / BIP Errors (per interval) - a new delta chart for external ONTs that report those counters, plotted as per-interval deltas so a steadily-climbing cumulative total doesn't render as an unreadable ramp.
BIP and HEC error alerts - new ONT: BIP Error Spike and ONT: HEC Error Spike rules (Alerts & Schedule - Rules) covering the always-on error signals, so a link running with payload FEC disabled - where the FEC alert can never fire - isn't left without error-counter alerting. BIP's threshold adapts to FEC state (strict when off, relaxed when on, where BIP is just pre-FEC noise). The rules enable automatically once ONT monitoring is configured, and each ONT alert deep-links to the tab that shows the triggering device (SFP Stats for an attached module, ONT Stats for a standalone ONT).
Zyxel GPON-SFP PMG3000 provider - a read-only telemetry provider for the Zyxel PMG3000 GPON-SFP stick (a MaxLinear/T&W-based module), for GPON FTTH setups that terminate the fiber in an in-gateway SFP stick rather than a standalone ONT. It reports PON link state and optics (Rx/Tx power, temperature, voltage, bias current) plus serial identity over the stick's plain-HTTP CGI API, no separate box required. (Add Zyxel GPON-SFP (PMG3000) ONT provider #1033, thanks @Optic00)
Provider-aware default host on the ONT form - picking a provider on the Add/Edit ONT form (Settings - ONT Device Monitoring) now suggests and fills that device's known management address (AT&T 192.168.1.254, Nokia and Telekom 192.168.100.1, 8311 192.168.11.1, Quantum 192.168.0.1, Zyxel 10.10.1.1), the way the port default already worked, instead of the same generic placeholder for every provider. A host you typed yourself is never overwritten.
Security Audit
/api/statusendpoint, keeping the previous HTML page scrape as a fallback for older Technitium releases. A Technitium resolver is identified more reliably as a result. (Technitium DNS: prefer /api/status for provider detection #1023, thanks @jimstrang)Multi-Site
On-site agent tunnel behind your own reverse proxy - The agent tunnel is gRPC, and our canonical-host redirect was 302'ing it whenever a reverse proxy forwarded the tunnel path without the original Host header. A gRPC stream can't follow a redirect, so the tunnel silently failed while REST heartbeats kept the agent showing online. This bites proxies that rewrite Host by default (nginx) or present the upstream authority on an HTTP/2 backend (Caddy); Traefik's defaults happened to avoid it. gRPC is now exempt from the redirect, so the tunnel connects no matter how your proxy handles the Host header.
Honest agent online/offline status - An agent now reads Online only when its tunnel is genuinely up (or briefly reconnecting), not merely because REST heartbeats are landing. A broken or unpublished tunnel - where monitoring relay, config push, the proxied UniFi Console, and relayed speed tests are all down - now shows Offline and raises the offline alert instead of a misleading green. Single-box installs with no tunnel listener are unaffected.
Add-a-Site wizard resets when you configure a site - after the Add-a-Site wizard reaches its "... is ready" success screen (Settings - Multi-Site), opening any site's Configuration from the Sites table now returns the wizard to a fresh form rather than leaving the completed screen sitting beside the site you're configuring. It stays put while you're mid-way through adding a site, so nothing in progress is discarded.
Fixes
Nokia XS-010X-Q ONT monitoring - Some Nokia XS-010X-Q firmware answers our login with a malformed Set-Cookie header that made .NET silently drop the hand-set session cookie, so the ONT returned 401 and never connected. We now disable the client cookie engine (the real session token arrives in the response body) with a byte-for-byte raw-socket request-replay fallback for any still-pickier firmware, so these ONTs report stats. (Nokia ONT: disable handler cookies (root cause) + raw-socket curl replay fallback #1029, thanks @jakerobb for isolating the root cause and field-testing)
Device names on the LAN Flow Map and Client Performance - Devices the UniFi Console has a friendly or fingerprint name for, but that you never manually renamed, were showing as a raw MAC on the 2D/3D LAN Flow Map (Monitoring - Live View) and on Client Performance - even though Wi-Fi Optimizer - Client Stats already displayed the proper name. Both now also read the UniFi Console's system-selected display name from the v2 active-clients endpoint and use it as the top of the label fallback chain (display name, then name, then hostname, then MAC), so an unnamed PlayStation or Fire tablet reads as its device name instead of a MAC. The name is served from a small per-site cache (5-minute TTL), so this adds no meaningful load on the console.
Duplicate Health Issues entries - On Wi-Fi Optimizer - Overview, the Health Issues card intermittently showed every finding twice (doubled rows and
(2)count badges) when the page's initial load overlapped its auto-refresh. The two concurrent health-score refreshes were sharing and mutating a single score object, so each finding got appended twice. Refreshes are now serialized, and the score is assembled in isolation and published only once complete, so findings appear exactly once.Spurious "disposed object" console-connection error - When a UniFi Console connection was re-established while a login was still in flight (for example an agent-tunnel site's console coming back after a restart), the console-connection warning could surface
Cannot access a disposed objectinstead of a normal transient retry. The login now degrades gracefully in that reconnect window and self-heals on the next poll.ControlD gateway-probe diagnostics (omit from release notes) - The on-gateway ControlD detector (
verify.controld.comCNAME probe) now logs at Debug when it starts, and on a no-match logs the actual CNAME/A records the resolver returned, instead of returning silently. This makes it possible to tell from a reporter's logs whether the probe ran and what came back. Diagnostic logging only, no behavior change (the probe still only runs when DoH is otherwise unconfigured).