v2.0.1 Release Changes#976
Merged
Merged
Conversation
…al audit log) (#970) * Agent: enforce site-local proxy dial policy with operator pinning and dial audit log The tunnel TCP proxy previously dialed any host:port the server named. The agent now owns a dial policy nothing on the tunnel can widen: targets must be site-local (RFC1918 / IPv6 ULA / IPv6 link-local), closing the internet-relay vector for a compromised central server. An optional proxyAllowedCidrs list in agent.json fully replaces the fence (operator narrowing or public-target escape hatch; invalid entries abort startup). Hostnames are resolved once, every address validated, and the dial uses the validated addresses. Every dial is journaled agent-side, allow and deny. * ONT panel: spinner on Refresh button while polling (matches CM/Cellular panels) * Tidy proxy policy test fixtures * Fix device SSH password wiped on edit; keep password managers out of the form The repository update path copied SshPassword unconditionally, but the edit form deliberately sends it blank to mean unchanged - so any edit of a custom speed test device (rename, username change) silently cleared its stored SSH password. Blank now means keep, matching every other credential form. The device form's username/password inputs also gain the autocomplete and password-manager opt-out attributes the Settings forms already had. * Device SSH config: configured key path clears the stored password Matches the gateway and UniFi SSH settings pages: key auth and a stored password are mutually exclusive, so a broken key file fails loudly instead of silently falling back to a stale password (SSH auth tries key first, then password, so a leftover password masked key misconfiguration).
…e the step band (#972) A shift that lands mid-window leaves the skipped transition window's median at whichever level held the majority of samples, and noise can push it fractionally outside the [before, after] band. The strict between-the-levels gate then rejected a real step-up while its matching step-down was only caught because its transition median landed 0.01 ms inside the band. Only reject the skipped window when it is itself a stable level outside the band (a distinct plateau), not a transition.
* Multi-site: per-tab site context via sticky ?site= URLs The selected site lived only in a browser-wide cookie, so every tab shared one site and alert "View" links silently flipped the whole browser via the ?site=-to-cookie middleware. New model: - ?site= in the URL pins its tab: the interactive circuit pins its scope from the tab URL (Routes.razor), SiteTabSync keeps the selector in the address bar across in-app navigation, and site-context.js stamps it onto same-origin /api/ fetches, /api/ anchors (PDF download, logout), and the LAN flow maps' full-page jumps. - The no-site cookie becomes the browser default for URLs without a selector (new tabs, bare bookmarks), written only by an explicit switch in the UI - existing cookies keep working unchanged. - The ?site=-to-cookie middleware is removed; alert links pin only their own tab. - SiteSwitchService consolidates the four duplicated cookie-write+reload switch flows and stamps ?site= on reload targets; header switcher items are real anchors so middle-click opens a site in a new tab. - Full-page navigations carry the pin: ClientDashboard retries and device picker, login redirects (middleware + JWT challenge), logout, and the post-login returnUrl. Single-site instances are unaffected: URL stamping is gated on multi-site being enabled. * Multi-site: sessionStorage backstop for dropped ?site= selectors If a full-page navigation ever forgets to stamp the tab's ?site=, the server resolves the browser-default cookie and the tab silently reverts to that site. site-context.js now remembers the tab's site in per-tab sessionStorage and, on load, restores a dropped selector with a one-shot redirect when it differs from the cookie default - turning a silent revert into an auto-correct. sessionStorage is written only by ensureSiteParam (which runs only when multi-site is enabled), so single-site instances never remember a site and the backstop never fires. The redirect target always carries ?site=, so it cannot loop, and default-site tabs never pay a reload. * Multi-site: preserve #fragment across a site switch The switch path stripped the #fragment because the old switch reloaded the same URL, where a fragment-only difference is a same-document nav that doesn't reload. Now the switch always changes the ?site= query, so the full reload is guaranteed and the fragment can ride along - switching sites from an anchored spot (e.g. a highlighted setting) lands on the same spot on the newly selected site. Applies to both the dropdown switch and the middle-click new-tab href. * Settings: Threat Intelligence uses Global badge, not a title prefix Match the other instance-wide security cards (MaxMind, etc.): the card title is plain "Threat Intelligence" with a Global badge on the default site when multi-site is enabled, instead of the "Global Threat Intelligence" title. * Multi-site: ctrl/middle-click a site to open it in a new tab The site switcher dropdown items and the /sites cards now honor ctrl / cmd / shift / middle click to open a site in a new tab. The dropdown items used a Blazor @OnClick with a static preventDefault (needed so a plain click runs the switch instead of a no-op SPA nav), but that also killed the native new-tab on modified clicks; the /sites cards were plain divs, so they had no link to open at all. Both are now real anchors whose href is the site's pinned URL. A small handler in site-context.js intercepts only a plain left click - preventing default and doing the same cookie-write + full reload as before - and lets modified/middle clicks fall through to the browser natively (window.open from a Blazor Server handler would be popup-blocked). The switcher menu/backdrop containers are unchanged, so the mobile top-bar auto-hide fix (aa59952) is unaffected. Also drop the pedantic "UniFi Console this site connects to" tooltip on the site card console host. * Dashboard: show the current site name next to the header (multi-site) When multi-site is enabled, the Dashboard header shows the current site's name as muted text next to "Dashboard" (including the main site), so each per-site browser tab is identifiable at a glance. Single-site instances are unchanged. * Dashboard: fix site-name header rendering literal Razor; bump suffix to 1.1rem * Header: live-update the Console Connected/Disconnected indicator MainLayout read ConnectionService.IsConnected but never subscribed to its OnConnectionChanged event, so the top-bar status indicator only refreshed on navigation. On agent-proxied sites the console connects asynchronously after first paint (once the agent tunnel is up), so the header stayed "Console Disconnected" until the next navigation. Subscribe and re-render on the event. * CSS: align dashboard site-name suffix and stop the h1 focus ring on navigation Center the page-header h1 contents so the muted site-name suffix aligns with the title, and bump its gap. Also suppress the outline FocusOnNavigate draws around the page h1 on every navigation for non-keyboard focus (keyboard focus keeps it). * CSS: unconditionally drop the FocusOnNavigate h1 focus ring (fires on fresh load and touch)
…in site (#974) Alert "View" links only carried ?site= for non-default sites; default-site alerts went out with no site discriminator. With per-tab site context, a link without ?site= resolves the recipient's browser-default site cookie, which may point at another site - so a main-site alert could open on the wrong site. Always append ?site=, substituting the default slug "main" when the event's SiteSlug is null, so a View link always pins its tab to the alert's own site.
* Add flaky LAN target advisory to the latency chart When the LAN category is selected on the Latency & Packet Loss chart and a non-gateway/non-AP fabric target shows elevated loss over the visible window, show an advisory that the loss is usually a measurement artifact and offer to review/disable the target in Latency Targets (expands the card, scrolls to and highlights the row). Per-target dismissal persists via MonitoringTarget.LanFlakyHintDismissedAt. Detection runs in the chart JS on data already loaded (no new queries); the role gate and dismissal apply in Blazor. Additive nullable column; the new JS path is fully guarded so it cannot affect chart rendering. Refs #971. * Lower flaky LAN target flag threshold to 0.5% mean loss 3% was far too high for LAN loss - any sustained loss to a LAN device is abnormal, so flag at 0.5% mean over the visible window. * Flaky LAN advisory: Dismiss button, pause wording, name size tweak Dismiss control is a labeled button instead of an x; copy says 'pause' to match the actual Latency Targets control (there is no Disable); device names set to 0.9rem; drop the now-unused dismiss CSS rule. * Comment: say 'pausing' not 'disabling' to match UI terminology
…eveal (#977) * Fix mobile scroll lock after app backgrounding: viewport lock + resume unstick iOS can restore the app (PWA or Safari) with the window scrolled off origin; the body then swallows touch gestures and .main-content can't scroll until a tap breaks it loose. Restore the html/body viewport lock removed in 1b4aae5 - it was dropped to preserve native pull-to-refresh, which was replaced by custom PTR the same day, so nothing needs body scroll anymore. Scoped via :has(.app-container) so the login page still scrolls. Also snap the window back to origin on pageshow/visibilitychange as a second layer for cases the CSS lock can't cover. * Drop position:fixed from viewport lock; overflow:hidden is enough position:fixed on body disturbed the dynamic-viewport behavior the sticky top-bar hide animation rides on, making the nav-bar hide feel off on scroll down. overflow:hidden alone still blocks body scroll, and the pageshow/visibilitychange handler remains the primary backstop for the background-resume case. * Smooth the mobile top-bar hide: translateY instead of top:-100% The hide animated 'top' toward -100%, which resolves against the tall scroll container (~800px), so the ~65px bar cleared the screen in the first few ms of the 0.35s transition - an abrupt snap. Slide by transform: translateY(-100%) (100% = the bar's own height) over 0.4s so the full duration moves exactly one bar-height: a gradual slide. * Slow the top-bar reveal to 0.6s, keep the hide at 0.4s Split the transition timing: the base .top-bar rule governs the reveal (slower, 0.6s) and .top-bar-hidden governs the hide (0.4s).
…thout data loss or UI hangs (#978) * Agent: keep collecting through tunnel outages, replay buffered results on reconnect Probe and SNMP runners now live for the whole agent process instead of per tunnel connection, feeding a store-and-forward ResultBuffer (12 h / 64 MB caps, oldest dropped first). While the tunnel is down the agent keeps polling on the last pushed config - previously the runners were torn down with the connection and the outage window was simply lost. On reconnect the backlog drains in FIFO order, coalescing consecutive batches to cut server-side per-batch overhead and throttling to keep channel headroom for heartbeats and proxied console traffic. Teardown salvages unsent messages from the outbound channel so a drop or failed connect loses nothing. Server side: custom-OID results now land at their sample timestamps instead of write time (a replayed backlog would have collapsed onto the flush time), and samples older than 10 minutes skip the alert state machines so a replayed down/up sequence doesn't fire alerts hours late. Interface rates come out correct across the gap for free - the counter delta cache persists through the outage and samples carry their own timestamps. * Tunnel liveness watchdogs: fail fast on black-holed agent tunnels A WAN outage black-holes the tunnel TCP connection rather than resetting it, and neither side noticed for the full TCP timeout (~15 min). Server side that meant the registry kept the dead connection, IsAgentOnline() stayed true, the console never flipped to awaiting-agent, and every page of the site - including a site switch - hung on proxy opens into the dead tunnel. Server: drop a connection silent past 90 s (agents heartbeat every 30 s; LastMessageAt is stamped on every message). The drop runs the normal teardown, so the awaiting-agent fail-fast now engages within ~2 min of a real outage instead of ~15. Agent: force a reconnect when nothing arrives for 150 s (the server re-pushes configs every 60 s, so a healthy tunnel is never that quiet). Without this the agent's read loop also hung on dead TCP, delaying the reconnect - and the buffered-backlog flush - by the same ~15 min. Forced reconnects lose nothing: the outbound channel is salvaged and results keep buffering. * Site-scope LAN flow map view state; reload PWA on stale resume Two multi-site UX fixes: Map view state (camera, overlays, scrub span, signal-hint dismissal) was keyed by a fixed localStorage prefix shared across every site, so switching sites inherited the previous site's 3D camera and 2D overlays. SiteContextService.ScopeStorageKey now prefixes the key with the site slug for non-default sites; the default site keeps the bare key so existing saved state and single-site installs are untouched. Applied at all four map mount sites (Monitoring 3D/2D, dashboard Live View 3D/2D). PWA resume: backgrounding the app on mobile suspends the tab and kills its Blazor WebSocket, but on resume Blazor often doesn't surface the reconnect modal, leaving the dashboard on its stale prerender with frozen loading spinners until a manual refresh. A visibilitychange handler now reloads when the tab was hidden past ~30 s and Blazor isn't already handling the drop, so a long-backgrounded page recovers on its own. * Fast-fail proxy opens on a black-holed agent tunnel A dead-but-registered tunnel (WAN black-hole, before the 90s server watchdog drops it) left every console call dialing the loopback proxy and blocking the full OpenTimeout for an answer that never comes, so a site switch or page load froze the browser for up to ~90s. - Refuse the proxy connect immediately when the agent has been silent past 45s (well above the 30s heartbeat, below the 90s watchdog). - Shorten OpenTimeout 10s to 3s; a live agent answers a ProxyOpen in well under a second. * 2D LAN flow map: keep bottom devices clear of the scrubber bar _fitAll centered on the raw canvas midpoint, so the desktop scrubber bar (which overlays the stage bottom) clipped the lowest devices. Reserve the bar's height as a bottom inset and center in the timeline-free region, lifting the map a touch. Mobile is unchanged (the scrubber sits below the stage there). * 2D LAN flow map: tighten the scrubber-bar clearance The first pass reserved the bar height plus a breather plus the bottom margin, leaving ~16px of dead space above the bar. Center in a proper [top margin .. just above the bar] band with a 4px gap instead. Mobile is unchanged (no overlay, so the band is symmetric top/bottom). * Agent: bound the pre-hello handshake so a black-hole can't wedge it The inbound-silence watchdog only arms after the server hello arrives, so a link black-holed in the connect->hello window would hang the read on dead TCP for the full ~15 min OS timeout before the reconnect loop could retry. Cap the handshake at 20s and fail fast into a retry. * Agent buffer: ack-based store-and-forward so black-holes don't lose data The buffer dropped a message as soon as WriteAsync returned, but TCP reports a write into a black-holed connection as success (the bytes sit in the kernel send buffer and are discarded on teardown). So during a WAN black-hole the buffer drained into the void and an outage's worth of monitoring data was lost - confirmed by a DROP test: zero latency points landed for the 2-minute outage window on either external site. Now a result frame stays buffered until the SERVER acks it: - Each frame carries a monotonic sequence; the server sends a cumulative ResultAck once the batch is persisted. - The drain only PEEKS frames (never removes on send); MarkAcked trims the acked run, and an unacked frame replays from the oldest on reconnect. This deletes the salvage machinery (SalvageUnsentInto, _pumpInFlight, RequeueFront) - nothing to salvage when the buffer already retains everything unacked. Rolling-upgrade safe via a ServerHello.supports_result_ack capability: a new agent against an older (non-acking) server falls back to trim-on-send so it doesn't retain and re-flush its whole buffer every reconnect. Old agent + new server is unaffected (sequence 0 => no ack). * PWA resume: probe the circuit before reloading, don't reload on a timer The visibilitychange handler reloaded whenever the tab was hidden past 30s, discarding scroll position and in-page state even when the Blazor circuit had survived the backgrounding. Now it round-trips a lightweight [JSInvokable] Ping on resume and only reloads when the circuit is genuinely dead (the probe times out); a live circuit resumes seamlessly. * Live WAN chart: overflow visible so it isn't clipped by the card The base .card clips to its rounded corners (overflow hidden), cutting off the live WAN chart's overhang on both the dashboard and the Monitoring Live View. Scope overflow: visible to the WAN chart card and its body. * Proxy: circuit-breaker so a site switch during an outage doesn't hang The liveness gate can't trip until 45s (it must stay above the 30s heartbeat to avoid false-failing a healthy tunnel), so in the first ~45s of a black-hole every proxy open still ate the full 3s OpenTimeout. A site switch fires a burst of console + SSH opens that serialize, so the switch hung ~15s until the gate caught up. Now the first open timeout trips a per-site breaker; opens queued behind it fast-fail instantly instead of each blocking OpenTimeout, collapsing the burst to a single timeout. The breaker self-clears the moment the tunnel produces fresh inbound (recovery) - keyed on LastMessageAt, so no probe is needed - or when the 45s gate takes over. * Proxy: hold the open breaker past the liveness gate, no mid-outage re-probe The breaker's 15s hold expired mid-outage and let a fresh burst of opens time out (3s each) before re-tripping - a ~10s stall on any site switch that landed in that window, recurring every ~15s until the 45s gate took over. Hold the breaker 60s so it stays tripped continuously until the gate handles it; it still clears the instant fresh inbound arrives, so a healthy tunnel is never held (its next heartbeat lets opens through). * Live View: show a console-down banner instead of "not configured" on outage A down console (agent-tunnel outage) made the gateway device fetch throw, which flipped the whole panel to the "Monitoring not configured / set up monitoring" empty state - wrong and confusing, since monitoring IS configured and the console is just unreachable. Isolate that fetch so it no longer blanks the panel, and surface a console-down / awaiting-agent banner (with the connection's own message) so the staleness is explained. * Live WAN chart: periodically re-pull history to fill post-outage/startup gaps pollLive only appends the latest sample forward, and loadHistory ran only on mount and tab-refocus - so data backfilled into Influx after the buffer scrolled past that time (an outage's buffered samples replaying on reconnect, or the cold-start gap before data first flowed) never appeared. A 60s backfill timer re-pulls the window and merges it, keeping the newest live samples ahead of the re-pulled history so the smooth live edge never regresses. Live mode + foreground only; stopped during historic playback and teardown. * Console: fail fast on a black-holed agent tunnel instead of a 14s retry A console reached via an agent tunnel is dialed through a loopback proxy. When the tunnel is black-holed the proxy fast-fails the open, but the HTTP client's transient-failure retry sits ABOVE the proxy and stacked the full 2+4+8s (~14s) exponential backoff onto every request - so a switch to that site hung ~10-15s for the whole ~90s before the watchdog flipped the console to awaiting-agent. Agent-proxied consoles (loopback 127.0.0.1) now get a single quick retry; directly-connected consoles keep the full backoff, so a real transient blip there is still ridden out. * Console: flip to awaiting-agent on the proxy's dead-tunnel signal Retry-scoping cut per-call cost from ~14s to ~0.5s, but a site switch still ran ~12 console calls while the console reported connected (stale green) during the ~90s before the watchdog flips it - so the switch still took ~9s. Now the tunnel proxy, on the first open timeout of an outage, tells the site's UniFiConnectionService to flip to awaiting-agent immediately, so page renders short-circuit console calls at the IsConnected guard instead of each dialing the dead proxy. The agent is still registered so OnAgentTunnelDroppedAsync can't fire yet; this is a separate path that skips that bail, is idempotent, and the agent-connected hook re-establishes the console on recovery. * Banners: dismiss app-wide nudges globally, not per-site SystemSettingsService.GetAsync/SetAsync route to the CURRENT site's DB, so a banner dismissed on one site reappeared when switching sites. The PWA install nudge and the WiFi channel-analysis disclaimer are feature-level (not site data), so dismiss them globally via GetGlobalAsync/SetGlobalAsync, matching the prerelease-updates and iperf3-preferences convention. Site-specific dismissals (upstream-discovery results, per-site speed-test schedule nudges) stay per-site. * Gate console wait/connect on tunnel LIVENESS, not registration The real source of the slow site switch during a black-hole: every page awaits WaitForConnectionAsync before rendering, and its only agent early-out checked registration (IsAgentOnline). A black-holed tunnel stays registered until the 90s watchdog, so the early-out never fired and every page load polled the full 3s timeout - twice per navigation (prerender + interactive) = ~9s per switch, ~13s with the first breaker probe, and instant only once the watchdog unregistered the agent. The proxy/retry fixes never moved this because the render was stalled in the poll, not the dials. - AgentTunnelConnection gains the shared staleness primitive (StaleThreshold 45s / IsStale); the proxy's private copy is replaced. - WaitForConnectionAsync bails immediately on IsAwaitingAgent and treats a dead-but-registered tunnel as absent (HasLiveAgentTunnel). - The connect paths and Test Connection use the same liveness check, so reconnect attempts during the interim land in awaiting-agent instead of dialing the dead proxy and misreporting an SSL error. - The 60s config refresh no longer un-flips awaiting-agent mid-outage: ReconnectConsoleIfViaAgentAsync skips a stale connection (the log showed it "reconnecting its console" through the dead tunnel every 60s, resurrecting the delay the flip had just removed). Registration-only checks remain only where they belong (teardown bookkeeping in OnAgentTunnelDroppedAsync). * Flip an outaged site's console proactively, not on first dial The awaiting-agent flip only fired from the proxy-open TIMEOUT path, so a site nobody touched during the outage never flipped: its opens were refused by the stale gate (which didn't flip), its console stayed stale-green, and the first switch to it paid a dial-plus-retry on every console call - ~9s per page load - until the 90s watchdog unregistered the agent. The user's own site flipped early only because its open pages happened to dial it. - The tunnel watchdog now flips the site's console the moment silence crosses the stale threshold (checked every 15s), so every site of a black-holed agent goes awaiting-agent by ~60s with no dial needed. - The proxy's stale-gate refusal also fires the flip (belt-and-braces for a dial landing between watchdog ticks). - NoteProxyUnreachableAsync renamed NoteTunnelUnreachableAsync; the proxy's two flip paths share one helper. Coverage: 0-45s = breaker flip on first timeout (~3s); 45s+ = proactive watchdog flip; dial-before-tick = stale-gate flip. Recovery unchanged (agent reconnect re-establishes the console). * Report awaiting-agent, not a bogus SSL error, when a connect hits a dead tunnel A connect attempt against a black-holed tunnel collapses mid-TLS at the loopback proxy and parsed as "SSL certificate error: enable Ignore SSL Errors" - advice that can't apply to an agent-proxied console (those force ignore-SSL; no certificate can match 127.0.0.1). The banner flashed that error until the awaiting-agent flip corrected it. After a failed connect, if the console is agent-routed and the proxy says the tunnel is suspect (no agent, stale, or open-breaker tripped with no fresh inbound), land in awaiting-agent with its message instead. A genuine console-side failure over a healthy tunnel keeps its real error. * Settings: show Ignore SSL Errors as forced-on for agent-connected consoles The setting can't apply through the agent tunnel (the console is dialed at a loopback proxy endpoint its certificate can never match), and the code has always forced it on for that path. Show the checkbox checked+disabled with verbiage explaining why, instead of implying the toggle does something. Directly-connected consoles keep the normal toggle. * Settings: trim the forced-on SSL help text
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.
v2.0.1 Release Changes — rolling
devintomain.Multi-Site
?site=selector in its own URL rather than a single shared cookie, so you can keep different sites open in different tabs at once. The cookie stays as the browser-wide default for new tabs and bare bookmarks, written only by an explicit switch in the UI. Refresh, duplicate-tab, and reconnect reloads all stay on the tab's site; site-switch links open in a new tab on ctrl/cmd/shift/middle click.Monitoring Agent
agent.json(proxyAllowedCidrs) to narrow reach further or admit an exotic public-IP target. Every dial (allow and deny) is journaled agent-side. Hostnames are resolved once and the connection goes to the validated address, so DNS games can't split the check from the connect. The policy is agent-owned; nothing on the tunnel can widen it.Latency & ISP Health
Fixes