Skip to content

v2.6.3 Release Changes - #1123

Merged
tvancott42 merged 5 commits into
mainfrom
dev
Aug 9, 2026
Merged

v2.6.3 Release Changes#1123
tvancott42 merged 5 commits into
mainfrom
dev

Conversation

@tvancott42

@tvancott42 tvancott42 commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Rolls up three merged PRs for the v2.6.3 release.

Monitoring

Hovering any chart on a tab reads the same instant on all of them, tooltip and crosshair together, across Live View, Device Stats, SFP Stats, CM Stats, ONT Stats, Cellular Stats and Starlink Stats. Synced tooltips cap at five rows so a busy tab stays readable, the day is named once under the x-axis where the tick labels are times, and stacked charts sit closer together. Links out of a chart carry the WAN and the moment you were looking at. (#1121)

Cellular Stats

  • Tower distance is back - the estimate disappeared when polling moved to uiwwand, which reports no timing advance. It comes from get-cell-tower-info instead, verified against the qmicli reading it replaces.
  • Site and sector - the LTE cell id decodes to the eNodeB and its sector, so neighbours sharing a site read as such.
  • Reset Radio - a modem can settle on an LTE anchor that offers no 5G and stay there, where previously only a reboot helped. Cycling the radio forces a fresh tower selection in about seven seconds. Offered only when the network permits 5G and the serving cell is the one withholding it; disabled with a reason when a reset cannot help or would tear down a connection still being established, and hidden when 5G is up. Site Admin, behind a confirmation.
  • 5G NR placeholder - a 5G modem with no 5G connection explains why in the empty slot rather than leaving one box in a two-box grid.
  • Timing advance, cell id, tracking area, neighbour count and 5G availability are recorded, so a drift off a 5G-capable tower is dated rather than noticed days later.

ISP Health

  • Cellular no longer reports a downgrade on every window - in 5G NSA one poll writes both an LTE and a 5G point, so the newest point was always the LTE half. A connection that never left 5G was reported as having dropped, costing five points and labelling the Physical Link factor LTE. A downgrade is now the 5G series going quiet while LTE keeps reporting.

ONT Stats

  • SFP Monitoring opens the module the card is showing rather than the bare tab.

Network Performance

  • Upstream Path Discovery no longer attributes a regional aggregator to a tier-1 ISP. Discovery through the ordinary targets is unchanged, and an ISP that genuinely buys from that aggregator still finds it.
  • Latency Targets says whether the server or an agent probes its targets, and says both when both do. Run Probe opens Network Tools on the vantage that actually probes the listed targets, following the WAN filter when none are listed yet, and Add Target follows the filter while open.
  • WAN / Path lists and Multi-WAN Monitoring - Vantages are ordered WAN1, WAN2, WAN3.

Device Stats

Wi-Fi Optimizer

  • Significant Load Imbalance stops firing on counts too small to mean anything. The rule read the spread across APs as a ratio, which is scale-free - 3 clients against 1 scored exactly as 30 against 10 - so a two-AP home with four clients between them was told an AP was overloaded and to lower its TX power. It now also asks that the busiest AP carry at least 8 clients and lead the quietest by at least 4, so the claim is true in clients and not only in ratio.
  • The imbalance figure is measured about the APs' own average. It was measured about every client on the site, wired included, so with more site clients than AP clients the percentage grew with that gap rather than with any real imbalance.

Network Tools

  • DNS Lookup - resolve a name, or an address in reverse, from any vantage, for checking DNS is reachable and answering from where you expect.
  • Ping shows what a hostname resolved to, which traceroute already did per hop.
  • Verify and Run Probe links select the specific WAN vantage rather than the agent's first, and the picker shows it however the page was reached.
  • Device types read as names rather than raw internal values.

Security

  • The four provider monitors are behind the service-layer RBAC gate. Cable Modem, ONT, Starlink and Cellular were each built by the per-site registry and never resolved through DI, so the method security gate never applied to them. Saving, deleting, enabling and resetting were reachable by any role that could reach a caller. Writes now require Admin on that site, matching the site-admin-only Settings pages that drive them; reads require Viewer, matching the monitoring cards that show them.
  • Provider settings links on the Dashboard cards are Site Admin, matching the pages they lead to.

Fixes

  • One unreachable device no longer takes its site's console offline. The server gave up on a proxy open before the agent stopped dialing, so the agent's own answer for a dead host never arrived and every unreachable target was misread as a dead tunnel. The server now waits long enough to hear it, and only treats a timeout as a dead tunnel when the tunnel has also gone silent.
  • Performance Tweaks accepts firmware up to 5.1.29, with the warning and the gate agreeing.
  • Settings asks whether a device's management page is only reachable when directly connected, rather than asking the reader to work out their own topology.

Agent

The agent update is optional this release. DNS Lookup is a diagnostic tool, and from an agent vantage an older agent says so plainly rather than returning a wrong answer. Everything else works with an agent of any version, and the console fix is server-side only.

tvancott42 and others added 5 commits August 8, 2026 14:15
…xis, and links that carry where you came from (#1121)

* Monitoring: carry the WAN into Network Performance on single-WAN sites

A jump from a live tile or a map's analyze icon named no WAN when the site
had only one, so the destination kept whatever scope it was last left in and
the chart then refused the category the link had asked for: clicking ISP RTT
while Network Performance was saved on LAN landed on the LAN chart, and the
2D/3D analyze icons landed on ISP because Fabric is not offered in a WAN
scope. The WAN key is whichever WAN the live chart is showing, which is not
necessarily WAN1 - a single-WAN site can be on wan2.

Emit side:
- LiveWanScope.QueryFragment no longer gates on HasChoice. The gate assumed
  one WAN means one view; the destination splits that WAN from the LAN
  regardless of how many WANs exist.
- The Monitoring page now loads LiveWanScope on single-WAN sites too, as the
  dashboard panel already did. Without the options loaded there was no key to
  put on the link at all, which is why the Live tab's ISP Health tile also
  named no WAN there while the dashboard's did.
- The gateway fabric tile on both surfaces asks for All through a shared
  MonitoringLinks.FabricTarget, so a LAN target link stops landing in a WAN
  scope that hides the row it names.

Receiving side (Network Performance):
- The link's WAN applies whenever the console named any WAN, not only on
  multi-WAN sites. One WAN still has a scope to set.
- All is taken from the token rather than inferred from the resolved keys:
  on a single-WAN site "all" and that one WAN resolve identically, and only
  All keeps the LAN categories on the chart.

Multi-WAN is unchanged throughout: every gate that moved was already
satisfied there, and the All inference behind the new token check is the
same expression it was.

* Monitoring: carry the moment into Device Stats, and into the gateway latency jump

The gateway tiles on both Live surfaces were the only ones that arrived
somewhere with no idea what moment they came from. Gateway RTT and Loss
opened Latency and Packet Loss on whatever window it was last left framing,
and Gateway CPU, Memory and Temp opened Device Stats the same way - so a tile
showing a spike could land on some earlier stretch of the day, and a tile
parked on an instant during playback lost that instant entirely.

Both now carry ?at=, live marker or parked instant, the way the ISP and
Transit tiles already did:

- Device Stats reads it and frames its charts on it. New frameMoment /
  frameTrailing in device-health-charts.js, an hour wide rather than the
  latency charts' 15 minutes: temperature, CPU and memory drift over a shift,
  and 15 minutes of a slow climb reads as a flat line. frameTrailing stands
  down on a fresh mount, which already opens on a trailing hour.
- Gateway RTT and Loss go through the same 15-minute framing as the other
  latency tiles, since they land on the same charts.

Both links are built by MonitoringLinks (FabricTarget gains the moment,
DeviceStats is new), so the two surfaces cannot drift apart on it again. The
Monitoring page reads its moment from CurrentAtToken, extracted from
JumpToAnalysis; the dashboard's tiles have no timeline and always say live.

The devices tab keeps its own pre-select pair rather than sharing the
performance tab's: those are consumed and cleared by the latency charts'
mount, which would eat the instant before Device Stats ever mounted.

* Device Stats: name the day once, under the x-axis

The tick labels are times, and datetimeFormatter only grows a date where the
granularity rolls over - so any window that crosses no midnight, which is
most of them, said nowhere what day it was showing. ApexCharts has no option
for stamping the date once, so this is an axis caption rather than a tick: a
caption holds still, where the tick that would carry it moves with the
chart's width.

Reads "Aug 8", or "Aug 7 - Aug 8" when the window spans midnight, from the
same effective window the range controls already compute. Applied on load
only when the text actually changes, since updateOptions redraws and the
poll runs every few seconds, and before the mark layer redraws - that call
recreates the annotation labels its tooltips are bound to.

Device Stats only for now; the other Monitoring charts share the same axis
shape and can follow.

* Monitoring charts: name the day once under the x-axis, everywhere but ISP Health

The Device Stats trial moves into chart-axis-date.js and goes on the other
six chart sets: Latency and Packet Loss, Device Stats, SFP Stats, CM Stats,
ONT Stats, Cellular Stats and Starlink Stats. ISP Health is deliberately left
alone.

Suppressed at 24 hours and above, where the tick labels carry dates of their
own and the caption would only repeat them. Since every tab except Latency
and Packet Loss and Device Stats opens on 24h, most of them show nothing
until the window is narrowed - which is the point: the caption exists for the
windows that name no day at all.

An empty string is a real suppression, not a blank label: ApexCharts guards
the xaxis title on a truthy check, so nothing is drawn and no space is
reserved for it.

Each module hands the caption its own charts and its own effective window,
since that is where the presets, shift offsets and custom ranges live. The
charts callback takes either chart instances or the [chart, element] pairs
the mark layer already uses, so no module keeps a second list. Applied ahead
of each module's redraw: that redraw recreates the annotation label elements
the mark layer binds its tooltips to.

* Chart axis date: match the tick labels it sits under

12px/600 Helvetica in the same grey ApexCharts draws x-axis labels in, and
the day zero-padded the way a day tick is ("Aug 08", not "Aug 8"), so the
caption reads as one of the labels rather than as a heading beneath them.

The importers move to ?v=2: a transitive import bypasses Razor's content
hash, so the browser would otherwise hold the v=1 copy.

* Monitoring: the gap between stacked charts belongs to the stack

Twenty-one identical `style="margin-top: 1rem"` on Monitoring's chart cards
and two more on the device page, all saying the same thing: a chart card
following a chart card is spaced from it. That is one rule.

Scoped to .card-body deliberately. Threat Intelligence pairs two chart cards
side by side inside .threat-two-chart-row, which is not in a card-body, and
an unscoped sibling rule would have dropped the second card an inch below
the first.

Four keep their inline margin because they are not this case - each is the
FIRST card inside a wrapper, with no chart-card sibling to be spaced from:
the SFP PON and ONT error sections, the stats table chart-stats.js injects,
and the custom-field charts device-health-charts.js appends.

* Monitoring: close the gap between stacked charts

The 1rem was excessive - stacked charts read as one instrument, and an inch
of dead space between each of six of them pushed half the tab below the
fold. Gone rather than reduced: the chart cards carry their own header
padding, and ApexCharts leaves room around the plot.

The rule added a commit ago goes with it - consolidating the 23 copies is
what made this a one-line deletion instead of 23 edits.

The stats table keeps its gap (chart-stats.js): it is a table under the
charts, not another chart, and the break is what says so.

* Monitoring: drop the stats table's top margin too

It is a sibling of the charts in the same stack, and the charts already
carry plenty of space below the plot - which is the whole reason the 1rem
was excessive in the first place. No chart-card anywhere carries an inline
top margin now.

chart-stats.js renders the port stats table as well, so that one loses its
gap on the same grounds. Its importers move to ?v=7: a transitive import
bypasses Razor's content hash.

* Live View: keep the port stats table's gap

It renders through the same shared table as the chart tabs, but nothing
charted sits above it - it follows the filter chips - so it was the one
place that actually needed the 1rem, and dropping it there closed a gap that
was doing work.

On the container rather than back in chart-stats.js: every other caller
follows a chart and wants none.

* Live View: half the gap is enough over the port stats table

* Device Stats: hover one chart, read the same instant on all three

Temperature, CPU and Memory join an ApexCharts group, so the tooltip and
crosshair follow the pointer across all of them. That is the question being
asked of a device anyway - what else was happening when this climbed - and
answering it meant reading three charts by eye and hoping.

Trial, on this tab only. The custom-field charts are deliberately out of the
group: the sync is by data index rather than timestamp, and while the three
fixed charts come from one payload through alignedPoints - so index i is the
same moment on each - a custom field is plotted from its own array and would
put the crosshair somewhere else.

* Device Stats: trim synced tooltips, and bring the custom charts into the group

Two things the trial turned up.

A synced tooltip answers for a chart nobody is pointing at, and past a
handful of series it grows taller than the chart being read - covering the
thing the reader came to look at. Synced copies now stop at five rows and
mark the trim with an ellipsis; the hovered chart still lists everything.
Which copy is which comes from a document-level pointerover, because a
synced chart sees no pointer event of its own. Unknown counts as hovered:
with no pointer seen yet, the full list is the safer answer.

The custom-field charts can join the group after all. They arrive from their
own Influx query - same window, own rows - and ApexCharts syncs by data
index, so plotted as they arrived their crosshair pointed at a different
moment. customPoints re-keys them onto the health rows (nearest within half
a step, so a bucket boundary landing a moment off still matches instead of
blanking the series) and bridges gaps on the field's own cadence, so a
slowly-polled OID keeps the line it had rather than breaking between every
reading and drawing nothing.

chart-tooltip.js importers move to ?v=11.

* Synced tooltip: lift the ellipsis off the bottom edge

* Synced tooltip: put the ellipsis on the last row, not under it

As its own block it cost a row of height - which is most of what capping the
list was meant to win back. It now sits at the right end of the fifth row,
pushed there by margin-left:auto inside the row's existing flex.

* Monitoring: hover sync on every multi-chart tab

The Device Stats trial generalizes. Latency and Packet Loss, Device Stats,
SFP Stats, CM Stats, ONT Stats, Cellular Stats and Starlink Stats now each
put their stacked charts in an ApexCharts group, so pointing at one reads the
same instant on all of them. The identity comes from chart-sync.js and is
stamped inside each module's base options, so every chart a tab builds -
including the ones created later, like the custom-field and PON charts -
joins without a call site knowing about it.

Two charts are deliberately not in their tab's group, because hover sync
addresses a point by data INDEX and both are drawn from a second query whose
rows are their own:

- SFP's PON charts get their own group rather than the optics one. They sync
  with each other, which is what a PON reader is comparing anyway.
- Latency's WAN Throughput joins nothing. It is a different measurement on
  its own cadence, and its index i is some other moment entirely.

The tooltip stops trusting that index either way: a synced copy now resolves
against the x the hovered chart reported rather than reading its own x at the
index it was handed, so rows and hover dots answer for the moment under the
pointer even where two charts' points do not line up one for one. The
crosshair is still ApexCharts' own, which is why the two exclusions stand.

Device Stats moves off its hand-rolled wrapper onto the shared helper.
chart-tooltip.js importers go to ?v=12.

* Monitoring: bring PON and WAN Throughput into their tabs' hover sync

Both were held out because hover sync addresses a point by index and both
are fed by a second query whose rows are their own. Being outside the group
is the wrong answer to that: the WAN Throughput under a latency spike is
most of why it sits there, and PON counters beside the optics are one
reading of one link.

SFP's PON charts join the tab group properly rather than loosely: ponPoints
now places the counters on the module's own optics rows through alignedOnto,
so index i is the same instant and the crosshair lands where the tooltip
says. A module reporting PON with no optics rows keeps its own timeline and
simply does not sync, which is what it did before.

WAN Throughput joins as it is - there is no single timeline on that tab to
re-key onto, since every target carries its own rows. Its rows stay honest
because the tooltip resolves by the hovered instant rather than by index.

alignedOnto is the device-health customPoints logic, moved beside
alignedPoints now that two tabs need it; customPoints is a call to it.
chart-tooltip.js importers go to ?v=13.

* Latency & Packet Loss: keep WAN Throughput synced across a category change

Each category brings its own targets, and their row counts differ - so the
data index the hovered chart hands the throughput chart could land past the
end of its own array, where there is nothing to draw and the synced tooltip
simply did not appear. Flipping categories until the counts happened to fit
brought it back, which is what made it look intermittent.

Its series are now placed on the longest series on screen through
alignedOnto, so index i is the same instant and always exists. Falls back to
its own timeline when nothing is charted to key onto.

* Monitoring: make the hover sync fire at all - match the group's extents

The sync was intermittent and failed in BOTH directions at once, which ruled
out anything about indices. ApexCharts passes a hover to a grouped chart
only when

    a.w.globals.minX === i.w.globals.minX && a.w.globals.maxX === i.w.globals.maxX

- an exact match on both ends. Charts drawn from one query agree by
construction, which is why Device Stats looked fine; a chart answering a
second query lands on the same first and last timestamp only by luck, so WAN
Throughput and the PON charts synced or did not depending on where their
data happened to start and stop. Changing category reshuffled that, and
flipping back through the categories eventually landed on a match again.

Each tab now stretches one series per chart to the group's extents with null
points (spanTo), so every chart reports the same minX and maxX. A null draws
nothing, takes no hover dot and gets no tooltip row, so the padding is
invisible.

The re-keying added in the last two commits goes with the theory that
prompted it. Apex resolves the hovered point per chart from the pointer's
position against that chart's own grid, so no chart ever needed its points
moved onto another's timeline - and doing it resampled a measurement onto a
cadence that was not its own. WAN Throughput, the PON counters and the
custom-field charts are back on their own rows, and alignedOnto is gone.

* Monitoring: anchor grouped charts to the window, not to their data

Still flaky, and flaky in TIME rather than by filter - the same category
worked or did not across a refresh. Padding to the group's data extents
could not settle that: it only stretches a series to reach them, and each
query is polled on its own schedule, so whichever poller wrote last owned
the later final sample and the extents disagreed again. Which one that was
came down to timing, which is exactly what the symptom said.

Every chart on a tab now reports the window it asked the server for: points
outside it are dropped - a sample a second past the end, from a poller whose
clock ran ahead, is precisely what broke the match - and the first series is
padded to both ends with nulls. Two charts agree because they were built
from the same request, not because their data happened to line up.

* Revert "Monitoring: anchor grouped charts to the window, not to their data"

This reverts commit 0b08913.

* Monitoring: instrument the chart hover sync, both sides

Four attempts in, the sync is still intermittent and there is nothing to
read: the deciding test lives inside ApexCharts, comparing each grouped
chart's minX and maxX against the hovered one's, and nothing on the page or
in the logs says what those values are.

Client: __netoptSync.report() prints every grouped chart's extents, the
delta from its group's reference, and whether ApexCharts would sync it -
read straight out of the library's own registry, so it compares what the
library compares. __netoptSync.watch() logs only when a group's alignment
CHANGES, which is what a flaky fault needs: leave it running, use the WAN
pills, and the log names the moment it broke instead of the moment someone
thought to look. __netoptSync.stop() ends it. Console only, and only when
called.

Server: chart-data and wan-rate-chart each log their row count, first and
last instant, and the window asked for, so the two queries can be read
against each other for the same moment. Extents only - no target names or
addresses.

* Monitoring: trim grouped charts to the group's extents, not just pad to them

The watcher named it: latency-12 (WAN Throughput) reported -24000/0 and then
0/+3000 against its group. Its own query starts and ends at its own
instants, and spanTo could only reach outwards - so a chart whose data went
PAST the group's ends kept its own and stayed out of step, in both
directions, exactly as often as the two pollers disagreed.

spanTo now takes the whole series array: every series is trimmed to the
group's extents and the first is padded to both ends. Anchored to the
group's own data, computed once per pass - not to the clock, which is what
broke this outright last time by giving each chart a max a few milliseconds
apart.

* SFP Stats: PON follows the optics only when the ONT modules are alone on screen

With another module shown beside them, the optics charts are drawing a line
the PON charts have nothing to answer with, and a crosshair tracking across
the two claims a correspondence that is not there. The PON charts move into
a group of their own then, so they still follow each other, and rejoin the
optics group as soon as the ONT modules are the only ones selected.

Membership is set on both the registry entry and the chart's own config,
since ApexCharts reads one to find a group's members and the other to decide
what the hovered chart belongs to. Rebuilding the charts would mean a full
remount on every chip click.

* Monitoring: remove the chart sync diagnostics

They found the fault - a 24s/3s extents drift on WAN Throughput - and the
fix went in with it. __netoptSync and the two endpoint log lines come back
out of git if another one of these ever needs chasing (8aa5984).

* Monitoring: stop grouped charts inheriting each other's options

ApexCharts' updateOptions takes a fourth argument, updateSyncedCharts, and
it defaults to TRUE - so once these charts shared a group, every call was
also applying its options to the rest of the group. Positional settings are
the ones that show it: SFP's RX-solid/TX-dashed pattern reached the PON
charts and dashed TX Frames on GEM Frames, and the WAN comparison dash array
reached WAN Throughput and dashed its upload.

Every call in a grouped module now passes false. The event marks and the
axis date caption too: those were being handed to group-mates as well, which
would have drawn one chart's marks on another as soon as two disagreed.

Pre-dates nothing - this arrived with the grouping, which is why it reads as
a regression on this branch.

* Latency & Packet Loss: draw every compared WAN's throughput

Comparing two WANs was answered by one WAN's throughput - the primary's -
under a title naming it. The point of putting two WANs side by side is to
see them apart, so the chart now draws each selected WAN's download and
upload, fetched per WAN and told apart the way the RTT chart does it: the
color still says download or upload, the dash pattern says which connection,
in the same progression the pills and the RTT series use.

Series carry the WAN's own token from the pill bar. The card title drops its
single-WAN suffix while several are drawn, since naming one of them would
contradict the chart under it.

* Latency & Packet Loss: key the throughput dash to the WAN, not to draw order

Review of the change above. The pattern was taken from each series' place
among the ones drawn, so one WAN's request failing shifted every WAN after
it onto another's pattern - and the whole point is that a WAN wears the same
dash here as on the RTT chart. Keyed off its place in the filter now, the
way wanDashFor does it.

The pattern is also only pushed when it changes: updateOptions redraws, and
the poll comes round every few seconds.

* Suggested targets: call SpaceX by its name

AS14593 registers as "Space Exploration Technologies Corporation", which the
suffix strippers boil down to "Space Exploration" - a name nobody would
recognize as the operator behind Starlink when picking targets to monitor.

CleanOrgName gains a last alias step, applied to whatever the strippers
leave, so discovery and manual add both store the known name. The lighter
display pass carries it too, under both forms the two cleaners can produce,
so names already stored read right without re-discovery.

Exact-match on the whole stripped name, like the brand overrides beside it:
a firm that merely starts with those words keeps its own.

* Network Performance: rename a target in place from Latency Targets

Renaming meant deleting the target and adding it again, which threw away its
history to fix a typo. The name cell now carries the same inline edit the
Sites table has - pencil, input, Save and Cancel, Enter to save, Escape to
back out.

Operator, the role that adds a target: naming one is curating the list, not
administering the site. The gate is on IMonitoringTargetService, so the role
is enforced and the change audited from and to whatever the name was; the
card's SiteOperatorOnly only decides whether the pencil is drawn. The name
is validated the way AddAsync validates it, so a rename cannot set one that
adding a target would have refused.

The inline-edit styles the Sites table introduced are now named for what
they do, and shared rather than copied.

* Inline rename: keep Save and Cancel together when they wrap

In a table cell there is rarely room for the field and both buttons, and
wrapping them one at a time left Cancel stranded on a line of its own. They
are one group now, so they drop to the next line together - in Latency
Targets and in the Sites table both, off the shared rule.

* ISP Health: analyze the window Per-Network RTT is showing

The chart plots per-ASN latency clusters, which Latency and Packet Loss
charts as Transit - but getting there meant switching tab, category and WAN
by hand and then finding the stretch again. The card gains the same
magnifier the Live surfaces carry.

It hands over the window rather than a moment: the instant at the center of
the current zoom, and how wide that zoom is. A reader who narrowed to one
evening did that to say WHICH evening, and landing on a fixed 15 minutes
would drop exactly what they had expressed. The zoom answers first, then the
report window, then what the chart drew.

?span= is an override and nothing more - every link without one frames what
it always framed. The WAN rides along as it does everywhere else.

* Latency Targets: keep a refused rename in the box

The edit closed before the save was attempted, so a name the service refused
- over the length limit, or from a role without the permission - left its
reason under the table with nothing to correct: the typed name was gone and
the row had reverted. It closes on success now, and stays open otherwise.

* ISP Health: pull the analyze magnifier flush with the card edge

* Guided tour for v2.6.3, and a predicate for a site that monitors anything

Two steps: the hover sync across a tab's charts, and renaming a target in
place. Both gated on has-targets - a new predicate for monitoring being on
with at least one enabled target of any type, which isp-health could not
answer: it wants an Access ISP target, so a site watching nothing but its
own switches and APs was turned away from steps its charts can show.

The rename step is optional as well: a Viewer sees no pencil, and the RBAC
gate is authored per step until the engine has one of its own.

The hover-sync anchor sits on the chart rather than the card - the sync is
something the plot does under the pointer, and it reads as that only with a
line to hover. The rename anchor is the table, not a row: which target comes
first depends on the site.
* Multi-Site: an agent on the UniFi gateway gets the gateway update instructions (#1107)

* Agent list: recognize a gateway agent on the main site

The upgrade one-liner a site's agent row offers was picked off a per-site
verdict that is false for the main site twice over: the call site guarded on
!site.IsDefault, and AgentOnGatewayDetector.IsAgentOnGatewayAsync answers
false for the default slug by contract. A main-site agent running on the
gateway was therefore handed the bare-metal, Docker and Proxmox LXC commands,
none of which apply to it.

Ask the per-address question instead - MatchGatewayAddressAsync, which is
deliberately not default-site gated - and key the answer by agent rather than
by site, since a site has one gateway and only one of its agents may be on it.
The site-level verdict keeps its existing contract, so the speed-test surfaces
that rely on it are untouched.

An agent with no open tunnel falls back to its last known LAN IP: an outdated
agent is exactly the one whose upgrade command this drives, and it may well be
the one that is down.

* Agent list: make gateway detection survive a console that is not up

The per-agent question was asked with MatchGatewayAddressAsync, which
resolves the gateway addresses live and has no memory. Those addresses come
from the site's UniFi Console, and on an agent site the console reconnects
through that agent's own tunnel - so opening the panel after a restart or an
agent update asks before the console can answer, the comparison set is empty,
and every agent resolves to a silent no. A gateway agent was then handed the
bare-metal, Docker and Proxmox commands. The site-level verdict never showed
this because it seeds from a persisted answer and keeps the last one when the
console is down, which is why WAN Speed Test stayed right on the same site at
the same moment.

Give the per-agent question the same durability: a persisted verdict per
agent, seeded before the refresh starts, with a degraded refresh keeping the
last answer instead of inventing a no. Deliberately does NOT wait on the
console - Settings is where a broken or unconfigured console gets fixed, so it
has to stay responsive there.

The existing entry points are untouched, so the speed-test consumers keep the
site-level contract exactly as it was. The default site now reaches the
persistence path for the first time, so it writes to the main database rather
than asking for a per-site one it does not have.

* TODOs for the remaining agent-on-gateway detection cleanup (#1106)

Marks the call sites still using the live, memoryless MatchGatewayAddressAsync
where the durable per-agent overload belongs, the one that needs the matched
address rather than a yes/no and so does not drop in, the narrow gateway
address set, and the uncovered first resolve.

Comments and TODO.md only - no behavior change.

* TODO: keep the address-set example generic

* Record the known gaps the regression review turned up (#1106)

The Any() reducer in SiteHasGatewayAgent: its one caller mirrors what a blank
override resolves to, and SiteSpeedTestTargetResolver resolves against one
agent, so a site with a gateway agent and a real-box agent makes the two
disagree. Secondary sites only - the main site does not render the field.

The per-agent key also puts every already-enrolled gateway agent through the
cold-start window once, since nothing reads the site-level row back under the
new key. Noted alongside why backfilling it is not worth building.

* Adaptive SQM: give the boot script room to install its dependencies

A first deploy runs the boot script over SSH with no timeout argument, so it
inherited the 30 second default. The script installs its dependencies inline
before it does anything else: it adds the Ookla packagecloud repo, which runs
its own apt-get update and fetches a GPG key, then apt-get installs speedtest,
bc and jq. On a cold apt cache or a slow WAN that comfortably outruns 30
seconds, and the deploy then tore down a deployment that was still working and
reported that the boot script had failed.

Give that one command five minutes, which clears a slow first install without
leaving the page sitting on a boot script that has genuinely wedged. Re-deploys
skip the whole block once the dependencies are present, which is why this only
ever bit the first attempt.

(cherry picked from commit cb8400b)

* Add UPS-2U-Pro device support

* Classify UniFi power devices as SmartPower

* Display Smart Power device type with spacing

* Address SmartPower review feedback

---------

Co-authored-by: TJ @ Ozark Connect <109822114+tvancott42@users.noreply.github.com>
Co-authored-by: TJ da Tuna <tjvc42@gmail.com>
…nd per-WAN vantage fixes (#1122)

Cellular monitoring lost its tower distance estimate when polling moved to
uiwwand, which reports no timing advance. It comes from get-cell-tower-info
instead, verified against the qmicli reading it replaces, and the LTE cell id
now decodes to a site and sector so neighbours sharing a tower read as such.
Timing advance, cell id, tracking area, neighbour count and 5G availability
are recorded, so a drift off a 5G-capable tower is dated rather than noticed
days later.

Reset Radio recovers a modem that has settled on an LTE anchor offering no 5G,
where previously only a reboot helped. It cycles the radio for about seven
seconds to force a fresh tower selection, and is offered only when the network
permits 5G and the serving cell is the one withholding it - a reset cannot help
when the network is gating it, and would tear down an NR leg still negotiating.
Site Admin, behind a confirmation, since the modem cannot tell whether it is
the active WAN. A 5G modem with no NR leg now explains itself in the empty
slot rather than leaving a single box in a two-box grid.

ISP Health reported every 5G NSA link as downgraded to LTE. NSA writes an LTE
and a 5G point per poll, so the newest point is always the LTE half; a
downgrade is now the 5G series going quiet while LTE keeps reporting.

Upstream Path Discovery attributed a regional aggregator to tier-1 access
ISPs. The carve-out probe exists to surface transits the ordinary targets
never touch, and the near-transit walk that normally rejects it stops at the
first tier-1 - which never fires when the tier-1 IS the access network, since
access hops are skipped first. Discovery through the ordinary targets is
unchanged, and an ISP that genuinely buys from that aggregator still finds it.

Network Tools gains DNS Lookup from any vantage, forward or reverse, built on
a parser covering the four nslookup dialects a UniFi estate actually produces.
Ping reports what a hostname resolved to, which traceroute already did per
hop. The vantage deep link now selects: links carry the specific WAN vantage
rather than the agent's first, and the picker applies it once the data it
needs exists rather than losing it to a slow vantage list. Device types read
as names rather than raw enum members.

The four provider monitors are behind the service-layer RBAC gate. Cable
Modem, ONT, Starlink and Cellular were each built by the per-site registry and
never DI-resolved, so the method security interceptor never applied - and
because none was marked as a mutating service, the architecture tests could
not see them either. Saving, deleting, enabling and resetting were reachable
by any role that could reach a caller. Writes now require Admin on that site,
matching the site-admin-only Settings pages that drive them; reads require
Viewer, matching the monitoring cards that show them.

One unreachable device could take its site's console offline. The server gave
up on a proxy open two seconds before the agent stopped dialing, so the
agent's own answer for a dead host never arrived and every unreachable target
was misread as a dead tunnel. The server waits long enough to hear it, and
only treats a timeout as a dead tunnel when the tunnel has also gone silent.

Also: Latency Targets names what actually probes its targets and follows the
WAN filter for both Run Probe and Add Target; WAN pickers and the vantages
table order WAN1, WAN2, WAN3; the ONT card's SFP Monitoring opens the module
it is showing; Performance Tweaks accepts firmware to 5.1.29; and the tour
gains DNS Lookup and Reset Radio steps with a cellular predicate.

The agent update is optional. DNS Lookup from an agent vantage needs the new
binary and an older agent says so plainly rather than answering wrongly;
everything else works with an agent of any version.

Two things deliberately kept out: no session link, and no real cell identifiers — the branch history was scrubbed of a geolocatable eNB earlier, so this must not reintroduce it.
The rule fired on the coefficient of variation alone, which is scale-free:
3 clients against 1 scores exactly as 30 against 10, both landing on the
50% threshold. So a two-AP home with four clients between them was told an
AP was overloaded and to lower its TX power, when a single device roaming
either way flips the verdict.

Two absolute floors alongside the existing ratio: the busiest AP carries at
least 8 clients, and leads the quietest by at least 4. The first makes the
"overloaded" claim true in clients rather than in ratio; the second keeps a
gap one roam can close from counting. Placed before the RF-distance branch
so the Info downgrade inherits them too.
The mean came from ctx.Clients.Count over the AP count - every client on
the site, wired included - while the deviations came from each AP's own
client count. Spread measured about a foreign centre is not a coefficient
of variation: with more site clients than AP clients every AP sits below
the mean, so the figure grows with that gap rather than with any real
imbalance, and the percentage the issue prints was wrong whenever the two
populations differed.

Both now come from the same list. Covered by a case that clears the count
floors so only the arithmetic decides: 8 clients against 4 with 36 on the
site reads 68% about the site's mean and fires, and 33% about the APs' own,
which is correctly silent. That test fails without this change and is the
only one of the eighteen that moves.
@tvancott42
tvancott42 merged commit debd4e2 into main Aug 9, 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.

2 participants