From c574f6461201ce80efd80c2a3f5e9916cd8af5c8 Mon Sep 17 00:00:00 2001 From: "TJ @ Ozark Connect" <109822114+tvancott42@users.noreply.github.com> Date: Sat, 8 Aug 2026 14:15:58 -0500 Subject: [PATCH 1/5] Monitoring: hover sync across each tab's charts, the date under the axis, 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 0b08913150b07567884831344cb9613aa1552191. * 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 (8aa59847). * 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. --- .../Helpers/NetworkFormatHelpers.cs | 27 ++- .../Components/Pages/Monitoring.razor | 155 ++++++++++++------ .../Components/Pages/MonitoringDevice.razor | 4 +- .../Components/Pages/Settings.razor | 8 +- .../Shared/LatencyTargetsCard.razor | 74 ++++++++- .../Components/Shared/LiveViewPanel.razor | 10 +- .../Shared/Monitoring/IspHealthPanel.razor | 37 ++++- .../Endpoints/MonitoringChartEndpoints.cs | 2 +- .../Services/IMonitoringTargetService.cs | 9 + .../Monitoring/AsnResolutionService.cs | 6 + .../Services/Monitoring/LiveWanScope.cs | 10 +- .../Services/Monitoring/MonitoringLinks.cs | 47 ++++++ .../Services/MonitoringTargetService.cs | 20 +++ .../Services/Tours/TourPredicateResolver.cs | 35 ++++ src/NetworkOptimizer.Web/wwwroot/css/app.css | 48 +++++- .../wwwroot/data/tours/2.6.3.json | 32 ++++ .../wwwroot/js/cellular-charts.js | 28 +++- .../wwwroot/js/chart-axis-date.js | 71 ++++++++ .../wwwroot/js/chart-event-marks.js | 4 +- .../wwwroot/js/chart-stats.js | 2 +- .../wwwroot/js/chart-sync.js | 69 ++++++++ .../wwwroot/js/chart-tooltip.js | 48 +++++- .../wwwroot/js/cm-charts.js | 28 +++- .../wwwroot/js/device-health-charts.js | 106 ++++++++++-- .../wwwroot/js/isp-health-charts.js | 29 +++- .../wwwroot/js/latency-charts.js | 118 ++++++++++--- .../wwwroot/js/ont-charts.js | 32 +++- .../wwwroot/js/port-stats-table.js | 2 +- .../wwwroot/js/sfp-charts.js | 101 +++++++++--- .../wwwroot/js/starlink-charts.js | 32 +++- .../wwwroot/js/wan-live-chart.js | 2 +- .../Helpers/NetworkFormatHelpersTests.cs | 15 ++ .../Monitoring/AsnNameCleanupTests.cs | 4 + 33 files changed, 1050 insertions(+), 165 deletions(-) create mode 100644 src/NetworkOptimizer.Web/wwwroot/data/tours/2.6.3.json create mode 100644 src/NetworkOptimizer.Web/wwwroot/js/chart-axis-date.js create mode 100644 src/NetworkOptimizer.Web/wwwroot/js/chart-sync.js diff --git a/src/NetworkOptimizer.Core/Helpers/NetworkFormatHelpers.cs b/src/NetworkOptimizer.Core/Helpers/NetworkFormatHelpers.cs index 2f9c7cd97c..68c8053219 100644 --- a/src/NetworkOptimizer.Core/Helpers/NetworkFormatHelpers.cs +++ b/src/NetworkOptimizer.Core/Helpers/NetworkFormatHelpers.cs @@ -136,15 +136,29 @@ public static string PonVariantLabel(string? sfpPart, string? sfpVendor) "B.V", "N.V", "Pty", "A/S", "AB", "Oy", "AS" }; + /// + /// The last step of the boil-down: what a company is actually called, for the ones whose + /// stripped legal name is not the name anyone knows them by. Exact-match against the fully + /// stripped form, never a substring - the suffix passes have already run by the time this is + /// consulted, so a key is written the way the name comes out of them. + /// + private static readonly Dictionary OrgAliases = new(StringComparer.OrdinalIgnoreCase) + { + // AS14593 registers as "Space Exploration Technologies Corporation", which strips to + // "Space Exploration" - a target list nobody would recognize as Starlink's operator. + ["Space Exploration"] = "SpaceX", + }; + /// /// The storage-time ASN/org-name cleaner: strips industry suffixes (Communications, Telecom, /// Broadband, Networks, Services, Parent, Holdings ...) and legal forms (LLC, Inc, AB ...) off /// the tail ("Hisense Broadband Technologies Co Ltd" -> "Hisense", "Level 3 Parent, LLC" -> - /// "Level 3"). Applied once when a target's AsnName is persisted - auto-discovery - /// (UpstreamTracerService.CleanAsnName) and manual target add (LatencyTargetsCard) both call it, - /// so the two paths store identical names. This is the HEAVIER of the two ASN-name cleaners; - /// the lighter resolve/display pass that also carries brand overrides (e.g. Arelion Sweden -> - /// Arelion, applied without re-discovery) is AsnNameCleanup in AsnResolutionService. + /// "Level 3"), then applies to whatever is left. Applied once when a + /// target's AsnName is persisted - auto-discovery (UpstreamTracerService.CleanAsnName) and + /// manual target add (LatencyTargetsCard) both call it, so the two paths store identical + /// names. This is the HEAVIER of the two ASN-name cleaners; the lighter resolve/display pass + /// that also carries brand overrides (e.g. Arelion Sweden -> Arelion, applied without + /// re-discovery) is AsnNameCleanup in AsnResolutionService. /// public static string CleanOrgName(string? name) { @@ -163,6 +177,7 @@ public static string CleanOrgName(string? name) } } } while (changed && cleaned.Contains(' ')); - return cleaned.Trim(); + cleaned = cleaned.Trim(); + return OrgAliases.TryGetValue(cleaned, out var alias) ? alias : cleaned; } } diff --git a/src/NetworkOptimizer.Web/Components/Pages/Monitoring.razor b/src/NetworkOptimizer.Web/Components/Pages/Monitoring.razor index 82b9667498..8f9fe9f61f 100644 --- a/src/NetworkOptimizer.Web/Components/Pages/Monitoring.razor +++ b/src/NetworkOptimizer.Web/Components/Pages/Monitoring.razor @@ -596,15 +596,17 @@
-
+ @* Anchored on the chart rather than the card: the sync is something the plot does + under the pointer, and it only reads as that with a line to hover. *@ +

Round-trip time

-
+

Packet loss

- -
+

CPU

-
+

Memory

@@ -860,22 +862,22 @@

RX / TX Power

-
+

Temperature