Skip to content

v2.0.1 Release Changes#976

Merged
tvancott42 merged 9 commits into
mainfrom
dev
Jul 13, 2026
Merged

v2.0.1 Release Changes#976
tvancott42 merged 9 commits into
mainfrom
dev

Conversation

@tvancott42

@tvancott42 tvancott42 commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

v2.0.1 Release Changes — rolling dev into main.

Multi-Site

  • External sites keep monitoring through tunnel and WAN outages. When an external site's agent loses its connection to the server, the agent keeps polling probe and SNMP data locally and replays the backlog when the tunnel returns, so an outage no longer punches a hole in that site's history. Delivery is acknowledged end to end, so even a black-holed connection (one TCP still thinks is alive) can't silently drop the outage's data, and charts backfill the gap on their own once it lands.
  • Switching to an offline external site is fast, not a hang. A site whose agent tunnel has gone dark is now detected quickly and its console flips to "waiting for the site's agent," so switching to it or loading its pages stays instant instead of freezing while the browser waits on the dead tunnel. Its Live View shows a clear agent-offline banner instead of the misleading "monitoring not configured" setup prompt, and the SSL-errors setting is shown as forced-on (it can't apply through the agent tunnel) rather than a toggle that does nothing.
  • Per-tab site context. A browser tab is now pinned to a site through a ?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.
  • Alert "View" links pin their originating site. Every alert link now carries its site, including for the main site, so a notification always lands on the site it came from instead of following the recipient's browser-default. Following one pins only that tab and no longer rewrites the cookie for the whole session.
  • Site name shown next to the Dashboard header when multi-site is enabled (including the main site), so each per-tab site is identifiable at a glance.
  • Threat Intelligence "Global" badge on the main site replaces the longer inline heading.

Monitoring Agent

  • Agent-owned proxy dial policy. The on-site agent's tunnel TCP proxy now refuses to dial anything that isn't a site-local address (RFC1918 / IPv6 unique-local / link-local) by default, closing the internet-relay vector a compromised central server would otherwise get for free. Operators can pin an explicit CIDR/IP list in 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.
  • Latest agent version bumped to 2.0.1.

Latency & ISP Health

  • Flaky LAN target advisory. LAN latency charts now surface a dismissible hint when a non-gateway/non-AP fabric device shows packet loss that's usually a measurement artifact (Wi-Fi roaming, ping deprioritization) rather than a real problem, with a jump to that target's row in Latency Targets. Gateways and APs are excluded so real infrastructure loss is never hidden.
  • Path-shift detection accepts noisy transition windows. A step change whose mid-window transition median lands just outside the before/after band (from a small pre-shift dip) is no longer discarded; only a window that is itself a stable third level still disqualifies the step.

Fixes

  • Mobile scroll no longer locks up after the app is backgrounded, and the top bar's hide/reveal on scroll is smoother.
  • Returning to the app on mobile keeps your place. It only reloads when the connection is actually dead, instead of reloading on a timer after backgrounding.
  • Dismissed "install as an app" and channel-analysis banners stay dismissed across site switches instead of reappearing on each site.
  • Live WAN chart and 2D network map no longer clip at their edges. The WAN chart isn't cut off by its card, and the map's bottom row of devices clears the timeline scrubber.
  • ONT external refresh button now disables and shows a spinner while polling, preventing double-taps.
  • Per-device SSH password handling. Configuring a key path clears any stored password (matching the gateway/UniFi SSH settings pages) so a broken key fails loudly instead of silently falling back to a stale password; a blank password on edit means "keep", not "clear". The SSH fields opt out of password-manager autofill.

tvancott42 and others added 9 commits July 12, 2026 09:29
…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
@tvancott42 tvancott42 changed the title TBD v2.0.1 Release Changes v2.0.1 Release Changes Jul 13, 2026
@tvancott42 tvancott42 merged commit d66d58a into main Jul 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant