UI/transport latency display#2419
Merged
0pcom merged 18 commits intoskycoin:developfrom May 3, 2026
Merged
Conversation
The visor's TransportSummary already carries a smoothed RTT (LatencyMS, populated by transport-level ping/pong since skycoin#2401); the UI just wasn't reading it. - app.datatypes Transport gains an optional latencyMs field. - node.service maps transport.latency_ms (snake_case from the REST API) into the typed model. Both fetch sites in the service get the wire-up. - transport-list adds a "Latency" column on the desktop table and a row on the small-screen card view; sortable like the other columns. Visible on /nodes/<pk>/routing (short list) and /nodes/<pk>/transports/<page> (full list). - transport-details modal adds a Latency item under the data section. Display rule: render `<n> ms` when latencyMs > 0, else `-` (dmsg transports and freshly-added ones report 0 until the first RTT sample is recorded).
Two stylistic shifts on the routing/transports surface: - Pagination on /nodes/<pk>/transports went away. Visors typically have 10s of transports, not 100s; the paginator added clicks for no benefit and broke the natural sort/scroll flow. The short-list embed on /nodes/<pk>/routing keeps its slice (it's used as a preview with a "view all" link). - The "+" icon on the transport list now toggles an inline add-transport form anchored to the page instead of opening the CreateTransportComponent dialog. Same fields (remote PK, label, type, persistent toggle), same submit logic — just no modal. Field defaults / type pre-selection / persistent-list update path all mirror the dialog so the user-visible behavior matches except for the lack of a popup. The CreateTransportComponent file isn't deleted yet; the inline flow doesn't reference it but the file still compiles and is part of the build. Removing it is a follow-up once we're sure no other caller hits it. Same branch as the latency-display work; ship together.
The "are you sure?" modals on toggles that are trivially reversible
(flip back if you didn't mean to) were noise — every change forced
two clicks for one action. Replace with direct action + snackbar:
- transport-list.changeIfPersistent (single + batch persistent
toggle): drops the dialog, runs the persistent-list update
inline, snackbar on success/error.
- node-info-content.changePublicConfig: same — flip is_public,
snackbar.
- node-info-content.changeTransportsConfig (transport public-
autoconnect): same — flip, snackbar.
- node-apps-list.changeAppAutostart (per-app and selected-batch):
same — flip autostart, snackbar.
Destructive operations (delete transport / route, stop running
app, turn off / update visor) keep their confirmation modals.
Form-style dialogs (skysocks settings, label edit, reward-address
warning, etc.) also stay; this pass is just about the
"reversible-boolean wrapped in are-you-sure" set.
Builds on the inline-add-transport and pagination-removal commits
to make the routing/transports surface noticeably less noisy.
Three changes on the same theme — make the hypervisor UI navigable without dialog spam, and surface info that was already on the wire but hidden by the UI. Modals dropped (continuation of f60abe5): - delete transport (single + selected-batch): no confirm modal, snackbar reports completion / errors. Misclick recovery: re-add via the inline form. - delete route (single + selected-batch): same — routes regenerate on the next dial. - stop app (single + selected-batch): start it again to recover. Modals deliberately kept: visor turn-off / update (one-off, not part of broad manipulation), reward-address-empty warning (lose rewards eligibility), all form-style settings dialogs. Routes view (/nodes/<pk>/routing, /nodes/<pk>/routes): the table was reduced to source/destination/type which doesn't actually tell you what each rule does. Now mirrors the columns of `skywire cli visor route ls`: Key | Type | Local port | Remote port | Remote PK | Next RID | Next TpID | Keep-alive Routing data is already present in the route summary the API returns; just wasn't surfaced. Runtime logs dialog: was one-shot (fetch, render, stop). Add a live tail that polls every 2s and replaces the buffer (the runtime-logs endpoint is full-buffer, no `since` cursor). Toggle button in the footer (pause/play). Auto-scroll only fires when the viewport was already pinned to the bottom — reading history doesn't get yanked back down on the next refresh. Manual "refresh" button preserved alongside.
Each runtime-log entry already carries a monotonic log_line field
populated by the logstore hook. Use it as a cursor so live tailing
only ships newly-arrived entries.
Backend:
- logstore.Store grows GetLogsSince(since) returning
(entries, dropped, latest). dropped > 0 when the ring buffer
wrapped past the caller's cursor between polls — surfaces
naturally in the UI as a "skipped N entries" hint.
- Visor.RuntimeLogsSince(since) returns the new RuntimeLogsDelta
shape: {entries, latest, dropped}.
- RPC: RuntimeLogsSince exposed; client + mock stubbed.
- HTTP: GET /visors/<pk>/runtime-logs?since=N returns the delta
shape; without ?since the legacy full-buffer array is preserved
so existing curl/CLI consumers don't break.
UI:
- node.service grows getRuntimeLogsSince(nodeKey, since).
- node-logs component holds a logCursor (highest log_line seen)
and a totalDropped counter. Each poll passes the cursor as
?since=, appends only the returned entries, trims the buffer
to maxElementsPerPage, and updates the cursor to delta.latest.
- Initial load sends since=0 (full buffer), subsequent polls
fetch only the diff. The 2s cadence is unchanged but the wire
payload is now nearly empty during quiet stretches.
- Dropped count surfaces in a small banner above the log pane
when the visor reports the ring wrapped past us.
Adds an in-browser equivalent of `cli gotop` to the hypervisor UI,
plus the visor-process Go-runtime view as a second tab.
Backend:
- HostStats() backed by gopsutil/{cpu,disk,host,mem,net,process}.
Returns CPU%, memory + swap, root-mount disk, cumulative network
bytes/packets, host identity (hostname/os/platform/uptime), and
visor-process slice (RSS, num_threads, num_fds, open_conns, ...).
Best-effort: a probe failure on one subsystem leaves that field
zero rather than failing the whole call.
- HTTP routes:
GET /visors/<pk>/host-stats — psutil-style snapshot
GET /visors/<pk>/runtime-stats — Go-runtime stats (was RPC-only)
- RPC: HostStats exposed; RuntimeStats already existed.
- gopsutil/v3 is already vendored — no new dep.
UI (resource-monitor component):
- Collapsed by default. Polls 1s only while open, stops on collapse.
- Two tabs:
Host — CPU%, mem%, disk%, net rx/tx Bps, threads
Process — heap MB, goroutines, GC/sec
- 60-sample rolling window per metric (≈ 60s at 1s polling).
- Reuses the existing app-line-chart sparkline component.
- Network is rate-derived: server returns cumulative bytes,
client diffs across samples and skips the first reading so the
cold-start spike doesn't show.
- Embedded at the bottom of the visor detail page (under Traffic).
… browser)
Adds a top-level "Network" tab at /nodes/network mirroring what
`skywire cli sd` prints: a per-PK table of services + transport
counts (stcpr/sudph/dmsg/stcp) + UT online status, with country/
version filters, search, and the same color coding the CLI uses
(offline → red text; not-in-UT → red bg; online but fewer than 2
real transports → amber bg).
Backend (pkg/visor/api_network_view.go):
- HostStats's pattern: best-effort aggregation. Fetches three SD
types (proxy/vpn/visor), TPD all-transports, and UT uptimes
via the existing FetchServiceData plumbing. A failure on one
source yields a partial table rather than a hard error.
- 30s in-process cache so multiple UI clients + concurrent CLI
calls share a single fetch.
- HTTP route GET /api/network-view (hypervisor scope, not per-
visor). RPC: NetworkView() registered.
UI:
- New page under /nodes/network. Polls every 30s (matches the
server cache TTL). Tab added to the existing top-bar nav on
every neighboring page (node-list, services-health,
dmsg-settings, settings).
- Inline filters: search box, country (2-letter), version, min
transport count, online-only checkbox.
- Header counts (online / offline / not-in-UT) for at-a-glance
network health. Mobile card view in addition to the desktop
table.
…ache
Changes per recent UX feedback. Each is small but the combined diff
is bigger because they touch overlapping spots.
Right-bar (node-info-content):
- Removed the Health section (services/uptime tracker/autoconnect/
transportability) — too coarse to be useful, the dedicated
Services Health page covers the same ground.
- Removed the "Transport Visualizer" link — it's already a tab on
every neighboring page's top bar.
- Removed the Resource Monitor + Traffic data anchored at the
bottom; both moved (see below).
- Reward address: replaced the dialog button with an inline edit
pencil that toggles a small form. The external-explorer link
stays for non-xpub addresses; new collapsible "Reward rules"
section fetches and renders the embedded mainnet_rules.md
instead of pointing at an external article.
- Transport autoconnect + public visor: replaced the change
buttons with mat-slide-toggle right next to the labels.
- Router config: replaced the modal "change config" button with
an inline pencil + min-hops form.
- Runtime Configuration: collapsible like the Ports section. The
raw JSON loads on first expand and stays cached.
New surfaces:
- "Resources" tab on the visor page (/nodes/<pk>/resources). Wraps
the existing app-resource-monitor with [openByDefault]=true so
polling starts on entry. Resource monitor gains an openByDefault
input for that purpose.
- Traffic data block moved to the routing page, under Routes —
that's where the activity it summarizes actually originates.
Network view (cli sd in browser):
- Server cache 30s → 5min. SD/TPD/UT change at the minutes scale,
no need to re-aggregate every 30s.
- GET /api/network-view?refresh=true forces a fresh fetch (used
by a new Refresh button on the page).
- GET /api/reward-rules serves the embedded mainnet_rules.md as
plain text; UI's reward block fetches it for the new "Reward
rules" expandable.
MatSlideToggleModule wired into the app module so the new toggles
render correctly.
…y in header
The right-bar split-view was eating ~30% of the page width on every
tab. Replace it with:
- "Info" is now a regular default tab. The :key route redirects
to /info instead of /routing. The Info tab is no longer hidden
on large screens via onlyIfLessThanLg — it's a peer to the
others.
- Right-bar template + everything tied to showingInfo is gone
from node.component.{ts,html}. The active tab is full-width.
- Visor identity (label + PK) lives in the top bar, replacing the
"Visor details" title text. The label is the user-set one when
available, falling back to a short PK form. The full PK shows
below it via app-copy-to-clipboard-text, persistent across tab
switches so the user always knows which visor they're on.
Top bar gains pageHeaderLabel + pageHeaderIdentifier inputs that,
when set, take precedence over the translated titleParts text.
Mobile bar uses the label compactly. Other pages that don't set
the new inputs continue rendering titleParts unchanged.
- add a Chat tab to the visor page that talks to skychat through a hypervisor reverse proxy at /api/visors/<pk>/skychat/proxy/* so the browser can reach skychat same-origin without exposing :8001 - skychat default --addr flips from :8001 to 127.0.0.1:8001 (the docker integration configs already pin *:8001 explicitly so e2e is unaffected) - optional skychat password mirrors the hypervisor's user-store hashing scheme (salted SHA256, single-line <hex-salt>:<hex-hash> file). Set / changed / cleared via new RPC methods; persisted under the visor's LocalPath and wired into the launcher via UpdateAppArg so the app restarts with the right --password-file - when the password gate is on, hvui sessions still bypass it via a per-startup random X-Skychat-Internal-Token; the standalone :8001 surface stays gated normally
- collapsible password section in the Skychat tab (lock-icon toggle in the status row) that can set / change / remove the password gating the standalone :8001 surface - DELETE handler also accepts ?old_password=... so Angular's HttpClient delete (which doesn't carry a body) can send the credential - copy makes it explicit that the hypervisor session always bypasses the gate via the internal token — only :8001 visitors see the prompt
…gination Visor detail tabs reshuffled so each surface owns the controls it governs. Pagination is gone from the per-visor route + transport lists since visors typically have a handful, not hundreds. - new "Transports" tab between Routing and Apps. Holds the transport summary block (total + per-type breakdown) plus the autoconnect / public-visor toggles previously on the Info tab, followed by the full transport list (no slicing, no "view all" link) - "Routing" tab now hosts the Min-hops editor (moved from Info) and shows the full routes list inline (paginators + paginator-fixer classes deleted from route-list) - "Rewards" tab now manages the reward address inline (display + set/change/clear form + collapsible reward rules) — Info tab loses that section - Info tab is a read-only identity card again: basic node info, ports, runtime config. The component .ts shrinks accordingly - legacy /transports/:page and /routes/:page URLs redirect to the new tab routes so existing bookmarks keep working Skychat fixes: - "Chat" tab label → "Skychat" in i18n - composer flex layout fixed so the Send button is always visible - explicit ::ng-deep input/textarea text-color overrides so form fields aren't dark-on-dark - new peers sidebar derived from the live message stream (recent senders + the active recipient), with a tap-to-pick-recipient row
PK truncation
- network view shows the full 66-char visor PK in the table + small-
screen card; shortPk() helper deleted
- route-list shows full remote-pk and full next-transport-id (drops
the [short] / shortTextLength="5"/"7" props on app-labeled-element-text)
- skychat sidebar + message log show full peer PK; shortPK() helper
deleted; CSS uses word-break: break-all so 66-char keys wrap
- storage default label for unlabeled nodes is the full PK now
(was localPk.substr(0, 8))
Form-field contrast
- global ::ng-deep override in styles.scss for input.mat-mdc-input-
element / textarea: explicit white text + white caret + light-grey
placeholder + dim floating label and outline. The skychat-scoped
override is gone — the global rule replaces it.
DMSG settings → per-visor tab
- HVDmsgSessions added to the API interface (rpc_client / rpc_visor /
rpc_client_mock / rpc_hypervisor_proxy implementations) so a
hypervisor proxying for a remote visor can read its dmsg snapshot
- new per-visor routes:
GET /api/visors/{pk}/dmsg/sessions
POST /api/visors/{pk}/dmsg/connect-all
PUT /api/visors/{pk}/dmsg/sessions-count
these go through ctx.API so they work for the local visor and any
remote visor reachable via the hypervisor RPC
- DmsgSettingsService takes a pk arg now
- DmsgSettingsComponent is a per-visor tab body (no top-bar of its
own); reads the pk from NodeComponent.currentNode and starts polling
once a node is loaded
- new "DMSG" tab on the per-visor page (icon: hub, /nodes/<pk>/dmsg)
- removed from home tab bar in 4 components (node-list, network-view,
services-health, settings); legacy /nodes/dmsg-settings redirects
to the home node list
- settings.component.html selectedTabIndex shifts 4 → 5 to match the
Settings tab's new index after the DMSG removal
The home top-bar tab strip rendered with a wide unexplained gap mid-strip on some viewports. Force the wrapper divs to size by their content (flex: 0 0 auto) and use container-level gap instead of margin-right per tab, so the strip stays packed regardless of how many tabs are present.
The bundled Material Icons font is older than current; ligatures for icons added in 2020+ (health_and_safety, hub) don't substitute and the underscored icon name renders as raw text — that was the "unexplained gap" before Services Health on the home top-bar (the gap was actually the literal string "health_and_safety" rendered at ~150px). - health_and_safety → check_circle on the four home top-bars and the services-health page header - hub → device_hub on the per-visor DMSG tab
Top-level tab strip reordered. "Services Health" tab is now labeled
"Deployment" and sits after Network Visualizer; user-facing copy on
that page refers to "deployment services" rather than just "services".
A new "Resources" tab between Deployment and Settings shows fleet-
wide host stats at a glance.
Tab order:
Visor list · Rewards · Network · Network Visualizer ·
Deployment · Resources · Settings
Deployment / RSN stats:
- new GET /api/route-setup-nodes/stats handler (fans out to every
EffectiveRouteSetupNodes PK in parallel via the local visor's
DmsgHTTP RPC, parses each /stats body into StatsSnapshot,
returns [{pk, snapshot, error}])
- Deployment page polls that endpoint every 30s and renders one
card per RSN: success rate %, success/fail/in-flight counts, p50/
p95/p99 latency, last success / last failure timestamps, and
the top three failure-reason pills
Resources tab (home):
- new MultiVisorResourcesComponent at /nodes/resources
- fans out /api/visors/<pk>/host-stats per online visor every 5s
(catchError per stream so a single timeout doesn't kill the batch)
- table view: status dot, label/PK link to the per-visor Resources
tab, CPU%, mem% with absolute values, disk%, derived TX/RX rate
(diffed from cumulative byte counters), process RSS
- mobile card view with the same fields
- color buckets: ok < 70% < warn < 90% < bad
Adds a network-wide Transports tab that proxies the TPD's /metrics
endpoint through the local visor's DmsgHTTP RPC (HTTP fallback when
DMSG isn't ready). Two render modes:
- Compact (default): one row per transport id, full edge PKs,
sent/recv/total bytes, latency. Mirrors `cli tp metrics -tv`.
- Tree: visors as expandable parents with their transports as
children. Mirrors `cli tp metrics --tree`.
Day-window selector (1d / 7d / 30d) and a manual Refresh button
sit alongside the view toggle. 5min poll cadence — TPD metrics
roll up daily so anything tighter just hits the cached aggregate.
The home top-bar tab strip is now visually grouped:
[ Visor list · Rewards · Resources ] ← this hypervisor
[ Transports · Network · Network Visualizer · Deployment ] ← network-wide
[ Settings ]
A `group?: string` field on TabButtonData lets top-bar.component
insert a thin vertical separator wherever adjacent tabs differ in
group. Implemented as a `.tab-group-separator` span in the html
plus a 1px-wide rule in the scss.
Tab definitions deduplicated into utils/home-tabs.ts so all six
home pages (node-list, network-view, network-transports,
multi-visor-resources, services-health, settings) draw from the
same array. selectedTabIndex shifted accordingly:
resources 5→2, network 2→4, deployment 4→6, settings 6→7.
New backend handler:
GET /api/network/transports?days=N → []TransportMetric
Replaces the per-tab-open HTTP fetch on the hvui's Transports tab
with a long-lived CXO subscription. The visor now subscribes to a
TPD-side TreeStore feed; the hvui handler reads the cached snapshot
and falls back to DMSG-HTTP / HTTP only when the publisher hasn't
emitted a Root for the requested day window yet (which is the
normal state until the deployment is updated).
TPD side (pkg/transport-discovery/api/cxo_metrics_publisher.go)
- new MetricsCXOPublisher: 60s ticker recomputes /metrics for
days={1,7,30}, marshals JSON, Put()s to "metrics/days/<n>"
- built with TPD's master SK so the feed PK = TPD's main PK that
visors already know from Transport.DiscoveryDmsg
- listens on a separate DMSG port (skyenv.DmsgTPDMetricsCXOPort=51)
so it doesn't collide with the inbound stats aggregator on port 50
- nil allowlist (open feed) — same access policy as the HTTP /metrics
route it mirrors
- wired in cmd/svc/transport-discovery/commands/root.go alongside
the existing CXO aggregator init; both gated on --cxo + dmsg
Visor side (pkg/visor/api_tpd_metrics_subscriber.go)
- lazy-created TreeStore Subscriber to TPD's PK on the metrics CXO
port; first hvui-driven FetchTransportMetricsCXO call constructs
it, persistent thereafter
- prefix filter "metrics/days/" + OnUpdate timestamp tracking so
the handler can surface "last update" headers
- closeTPDMetricsSubscriber wired into Visor.Close so the dmsg
conn drops cleanly
Hvui handler (pkg/visor/hypervisor_handlers_tpd.go)
- 3-step fetch chain:
1. CXO subscriber cache (instant when fresh)
2. DMSG-HTTP via DmsgHTTP RPC
3. plain HTTP
- emits X-Skywire-Metrics-Source = cxo|dmsg-http|http so the path
taken is observable, plus X-Skywire-Metrics-Updated when CXO
Misc
- 'speed' Material icon (Resources home tab) → 'memory' — same
Material-Icons-font-version trap as the earlier health_and_safety
fix; 'speed' was rendering as the literal underscored string,
pushing the next tab off-screen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.