Skip to content

UI/transport latency display#2419

Merged
0pcom merged 18 commits intoskycoin:developfrom
0pcom:ui/transport-latency-display
May 3, 2026
Merged

UI/transport latency display#2419
0pcom merged 18 commits intoskycoin:developfrom
0pcom:ui/transport-latency-display

Conversation

@0pcom
Copy link
Copy Markdown
Collaborator

@0pcom 0pcom commented May 3, 2026

No description provided.

0pcom added 18 commits May 2, 2026 18:36
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
@0pcom 0pcom merged commit 96ee380 into skycoin:develop May 3, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant